😸

フロントエンドテスト 入門 Jest編

2023/05/27に公開

今回は、フロントエンドのテストについて解説していきます。

フロントエンドには、様々なテストの種類やツールがあります。

そして今回は関数のテストに絞って、Jestでのテスト方法を解説していきます。

  • React Testing Library編
  • Cypress 編

も出そうと思っているので、そちらも参考にしてください。

初めてのテスト

まず、テストで使うReactのプロジェクトを作ります。

npx create-react-app react-jest-practice --template typescript

インストールが終わったところで、さっそくテスト用の関数を作りましょう。

cd react-jest-practice
touch src/calc.ts
export const sum = (a: number, b: number) => a + b;

簡単な足し算の関数を作りました。

次にこの関数用のテストファイルを作っていきます。

touch src/calc.test.ts

.testをつけておくことで、yarn test時にJestが自動でそのコードを実行してくれるようになります。

import { sum } from “./calc”;

test("足し算", () => {
  expect(sum(1, 2)).toBe(3);
});

これで、次のコマンドを実行します。

yarn test calc.test.ts

無事に初めてのテストが成功しているかと思います。

イマイチ何をやったか分かってないと思うので、詳しく解説していきます。

test/it

まず最初に、testという関数について見ていきます。

こちらは、第一引数にテストの説明文、第二引数にテストの内容を書くことができます。

ちなみに、全く同じ機能でitというものがありますが、これは次の2つの様に英語的に変にならないようするためのものです。

test("1 + 2 to equal 3", () => {
  expect(sum(1, 2)).toBe(3);
});

it("should be 3", () => {
  expect(sum(1, 2)).toBe(3);
});

なので、テストの説明文を日本語で書くのであれば、基本的にtest関数を使っておけばOKです。

マッチャー

Jestでは、expect(処理内容).マッチャー(マッチする値)と書くことで、関数をテストすることができます。

例えば先ほどの例で言うと、toBeがマッチャーとなります。

また、expect().not.toBe()などとnotを使用することで、一致しないことをテストすることもできます。

よく使うマッチャーを表にまとめると、次のようになります。

マッチャ 意味
toBe ===による等価性のチェック
toEqual オブジェクトの中身が一致しているかのチェック
toStrictEqual toEqualより少し厳密なチェック
toBeNull nullかチェック
toBeUndefined undefinedかチェック
toBeDefined undefinedじゃないかチェック
toBeTruthy trueであるかチェック
toBeFalsey falseであるかチェック
toBeGreaterThan, toBeLessThan >,<かチェック
toBeGreaterThanOrEqual, toBeLessThanEqual >=,=<かチェック
toBeCloseTo 不動小数点の値が一致しているかチェック
toMatch 正規表現によるチェック
toContain 配列の中身をチェック
toThrow エラーの中身をチェック

describe

テストはdescribeという関数で、複数にまとめることができます。

例えば、新たに引き算の関数を作ったとしましょう。

export const sub = (a: number, b: number) => a - b;

これを次のようにまとめることができます。

import { sub, sum } from "./calc";

describe("加減算のテスト", () => {
  test("足し算", () => {
    expect(sum(1, 2)).toBe(3);
  });
  test("引き算", () => {
    expect(sub(2, 1)).toBe(1);
  });
});

こうすることで、テストが見やすく・分かりやすくなります。

ちなみに、describeはスコープを生成するので、この中だけで変数などを共有することもできます。

非同期処理のテスト

Jestでは普通に非同期のテストを実行しようとしても上手くいかないです。

例えば、以下のテストを試してみましょう。

touch src/async.ts
export const asyncFunc = async () => {
  return await fetch("https://qiita.com/api/v2/items");
};
touch src/async.test.ts
import { asyncFunc } from "./async";

test("the data is fetched", () => {
  const result = asyncFunc();
  expect(result.ok).toBeFalsy();
});

この場合、本当はtrueが返ってきてテストは失敗に終わるはずですが、実際にテストを実行すると成功してしまいます。

なので、普通のJavaScriptと同じように非同期処理を実行せさせたいなら、async/awaitなどを使用する必要があります。

import { asyncFunc } from "./async";

test("the data is fetched", async () => {
  const result = await asyncFunc();
  expect(result.ok).toBeFalsy();
});

もちろん、thenを使って書き換えることも可能です。

テストの初期設定

テストを実行する前に、クラスをインスタンス化したり、DBにデータを挿入する必要がある場合が多々あります。

この場合、全てのテストケースの中でそれを実行するのは少し面倒です。

なので、Jestにはテスト前にセットアップを行う用の関数が用意されています。

それが、beforeEachbeforeAlllです。

これは例えば、次のように使用することができます。

beforeEach(() => {
    //DBにデータを挿入
})

it("is updated", () => {
   // DBのデータをアップデート
   // DBの値が更新されているかテスト
})
it("is deleted", () => {
   // DBのデータを削除
   // DBのレコードが削除されているかテスト
})

このように、beforeEachはそれぞれのテスト前に実行されます。

ただ、今回のテストの場合は最初に1個データが作成されていれば十分です。

その場合は、全てのテスト前に一度だけ実行される、beforeAllを使えばOKです。

また、同じようにafterEach,afterAllでテスト後の処理を定義することもできます。

Mock

Jestでは関数をMock化することができます。

失敗した場合のテストや、日付が関係する関数のテストは再現が難しいです。

また、まだ未実装の関数に依存している関数を実行する時も、モックを作成する必要があります。

ちなみに、そもそもMockとは代用品という意味合いで理解していればOKです。

  • スタブ:決められた値を返却するもの
  • スパイ:呼び出し回数や出力を記録・確認するもの
  • モック:上2つを総称したもの

みたいな定義もありますが、これは覚えなくてもOKです。

では早速、Mockの作成方法を解説していきます。

基本的な置き換え

まず、基本的な置き換えの方法を見ていきます。

touch src/greet.ts
export function greet(name: string) {
  return `Hello! ${name}.`;
}

export function sayGoodBye(name: string) {
  throw new Error("未実装");
}

greetという関数は実装済みですが、sayGoodByeという関数は未実装になります。

なので今回は、sayGoodByeをMockに置き換えていきます。

まずはテストコードを見ていきましょう。

touch greet.test.ts
import { greet, sayGoodBye } from "./greet";

jest.mock("./greet", () => ({
  ...jest.requireActual("./greet"),
  sayGoodBye: (name: string) => `Good bye, ${name}.`,
}));

test("挨拶を返す(本来の実装どおり)", () => {
  expect(greet("Taro")).toBe("Hello! Taro.");
});

test("さよならを返す(本来の実装ではない)", () => {
  const message = `${sayGoodBye("Taro")} See you.`;
  expect(message).toBe("Good bye, Taro. See you.");
});

jest.mock("対象ファイル",コールバック関数);でMockを作成することができます。

また、requireActual関数で、本物の関数を設定することができます。

そして、それを分割代入することで、sayGoodByeだけをMockに置き換えることが可能となっています。

APIの置き換え

次にAPIの置き換え方法を見ていきます。

今回はaxiosというライブラリを置き換えていきます。

npm i axios
touch src/axios.ts
touch src/axios.test.ts
import axios from "axios";

export const asyncFunc = async () => {
  return await axios.get("https://qiita.com/api/v2/items");
};

export const firstPost = async () => {
  const result = await asyncFunc();
  const data = result.data;
  return data[0];
};
import axios from "axios";
import { firstPost } from "./axios";

const httpError: HttpError = {
  err: { message: "internal server error" },
};
type HttpError = {
  err: { message: string };
};

// axiosのモック化
jest.mock("axios");

describe("firstPost function", () => {
  it("テスト取得成功時", async () => {
    // モック化したaxios.getの返り値を設定
    (axios.get as jest.Mock).mockResolvedValue({
      data: [
        { title: "Post1", content: "Content1" },
        { title: "Post2", content: "Content2" },
        // 他のポストデータ...
      ],
    });

    const firstPostData = await firstPost();

    expect(firstPostData).toEqual({ title: "Post1", content: "Content1" });
  });
  test("データ取得失敗時", async () => {
    // getMyProfile が reject した時の値を再現
    (axios.get as jest.Mock).mockRejectedValueOnce(httpError);
    await expect(await firstPost()).rejects.toMatchObject({
      err: { message: "internal server error" },
    });
  });
});


このように、コードの先頭でモック化し、mockResolvedValueで返り値を設定することができます。

また、mockRejectedValueOnceでrejectされた場合のテストも再現することができます。

SpyOn

Jestの特徴的なモック機能の一つに jest.spyOn() があります。

jest.spyOn() は、オブジェクトのメソッドの呼び出しを追跡するために使用されます。

呼び出された回数、呼び出し時の引数、呼び出しの順序など、そのメソッドの使用情報を詳細に記録します。

さらに、 jest.spyOn() はメソッドの戻り値を偽装(モック)する機能も提供します。
たとえば、下記のような感じです。

const obj = {
  method: () => 'initial result',
};

test('spyOn mock implementation example', () => {
  const spy = jest.spyOn(obj, 'method').mockImplementation(() => 'mocked result');

  const result = obj.method();

  expect(result).toBe('mocked result');
  expect(spy).toHaveBeenCalled();
});

また、呼び出し回数なども記録できると言いましたが、それをコードで見てみると次のような感じになります。

const obj = {
  method: (arg) => `Called with ${arg}`,
};

test('spyOn detailed usage example', () => {
  const spy = jest.spyOn(obj, 'method');

  // First call
  const result1 = obj.method('arg1');
  expect(result1).toBe('Called with arg1');
  // Second call
  const result2 = obj.method('arg2');
  expect(result2).toBe('Called with arg2');

  // Check how many times the method was called
  expect(spy).toHaveBeenCalledTimes(2);

  // Check the arguments for each call
  expect(spy).toHaveBeenNthCalledWith(1, 'arg1');
  expect(spy).toHaveBeenNthCalledWith(2, 'arg2');

  // Check the order of method calls
  expect(spy.mock.calls[0][0]).toBe('arg1'); // First call was with 'arg1'
  expect(spy.mock.calls[1][0]).toBe('arg2'); // Second call was with 'arg2'
});

このように呼び出し回数や、呼び出しの引数などを調べることができます。

イマイチ使うタイミングが分からないかもですが、次のような時に使われます。

  • 外部ライブラリの関数や依存関係のある関数の呼び出しを追跡したい場合
  • 関数が正しい順序で、または正しい引数で呼び出されているかをテストしたい場合
  • ある関数が呼び出された回数をテストしたい場合

まとめ

今回はJestの使い方について解説してきました。

フロントエンドのテストでJestを使わないということはほぼないので、ぜひこの機会に身につけましょう。

宣伝

Zenn

0からエンジニアになるためのノウハウをブログで発信しています。
https://hinoshin-blog.com/

また、YouTubeでの動画解説も始めました。
https://www.youtube.com/channel/UCqaBUPxazAcXaGSNbky1y4g

インスタの発信も細々とやっています。
https://www.instagram.com/hinoshin_enginner/

興味がある方は、ぜひリンクをクリックして確認してみてください!

Discussion