[Jest] 呼び出す関数をconditionalに変えるコードのテストではclearAllMocks()を使おう

2021.07.01

こんにちは、CX事業本部の若槻です。

Jestは、Facebookが開発しているJavaScriptのテスティングフレームワークです。

今回は、呼び出す関数をconditionalに変えるコードのJestによるテストでの注意点についてです。

環境

% node --version
v12.14.0
% jest --version
26.6.3

モックした関数の呼び出し回数がテスト間でリセットされない

下記のようなmodule_bの関数を呼び出すmodule_aがあります。引数userTypeの値に応じて異なる関数が呼び出されます。

module_a.ts

import * as ModuleB from "./module_b";

export type UserType = "manager" | "general";

/**
 * ユーザーがデータを取得する
 * @param userType ユーザータイプ
 */
export const getDataByUser = async (userType: UserType): Promise<string> => {
  if (userType === "manager") {
    return await ModuleB.getDataByManager();
  }
  userType === "general";
  {
    return await ModuleB.getDataByGeneralUser();
  }
};

module_b.ts

/**
 * 管理者ユーザーがデータを取得する
 */
 export const getDataByManager = async (): Promise<string> => {
  return "data for manager";
};

/**
 * 一般ユーザーがデータを取得する
 */
export const getDataByGeneralUser = async (): Promise<string> => {
  return "data for general";
};

この時module_aに対する次のようなJestのテストコードを作成しました。module_agetDataByUser()が引数に応じてmodule_bの適切な関数を適切な回数呼び出せているかをテストする内容です。

test/module_a.test.ts

import * as ModuleA from "../module_a";
import * as ModuleB from "../module_b";

describe("module_a", () => {
  (ModuleB.getDataByManager as jest.Mock) = jest.fn().mockReturnValue("dummy");
  (ModuleB.getDataByGeneralUser as jest.Mock) = jest
    .fn()
    .mockReturnValue("dummy");

  it("By Manager", async (): Promise<void> => {
    ModuleA.getDataByUser("manager");

    //getDataByManagerが1回実行されることを期待
    expect(ModuleB.getDataByManager).toHaveBeenCalledTimes(1);
    expect(ModuleB.getDataByGeneralUser).toHaveBeenCalledTimes(0);
  });

  it("By GeneralUser", async (): Promise<void> => {
    ModuleA.getDataByUser("general");

    //getDataByGeneralUserが1回実行されることを期待
    expect(ModuleB.getDataByManager).toHaveBeenCalledTimes(0);
    expect(ModuleB.getDataByGeneralUser).toHaveBeenCalledTimes(1);
  });
});

しかしこのテストコードを実行すると下記のようにエラーとなってしまいます。1つ目のBy ManagerケースはPassしています。しかし2つ目のBy GeneralUserケースが、getDataByManagerの呼び出し回数が0回を期待しているところ実際には1回呼び出されてFailとなっています。

% jest ./module_a.test.ts

 FAIL  test/module_a.test.ts
  module_a
    ✓ By Manager (2 ms)
    ✕ By GeneralUser (2 ms)

  ● module_a › By GeneralUser

    expect(jest.fn()).toHaveBeenCalledTimes(expected)

    Expected number of calls: 0
    Received number of calls: 1

      20 |
      21 |     //getDataByGeneralUser が1回実行されることを期待
    > 22 |     expect(ModuleB.getDataByManager).toHaveBeenCalledTimes(0);
         |                                      ^
      23 |     expect(ModuleB.getDataByGeneralUser).toHaveBeenCalledTimes(1);
      24 |   });
      25 | });

      at Object.<anonymous> (test/module_a.test.ts:22:38)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        2.225 s
Ran all test suites matching /.\/module_a.test.ts/i.

原因、解決

原因はテスト間(it間)で関数の呼び出し回数がリセットされていなかったためでした。

そこでjestのclearAllMocks()を使用します。

下記のようにテストの冒頭にafterEachを使用してテストごとにclearAllMocks()を呼び出してモックの初期化が行われるようにします。

test/module_a.test.ts

import * as ModuleA from "../module_a";
import * as ModuleB from "../module_b";

describe("module_a", () => {
  afterEach(() => {
    jest.clearAllMocks();
  });

  (ModuleB.getDataByManager as jest.Mock) = jest.fn().mockReturnValue("dummy");
  (ModuleB.getDataByGeneralUser as jest.Mock) = jest
    .fn()
    .mockReturnValue("dummy");

  it("By Manager", async (): Promise<void> => {
    ModuleA.getDataByUser("manager");

    //getDataByManagerが1回実行されることを期待
    expect(ModuleB.getDataByManager).toHaveBeenCalledTimes(1);
    expect(ModuleB.getDataByGeneralUser).toHaveBeenCalledTimes(0);
  });

  it("By GeneralUser", async (): Promise<void> => {
    ModuleA.getDataByUser("general");

    //getDataByGeneralUserが1回実行されることを期待
    expect(ModuleB.getDataByManager).toHaveBeenCalledTimes(0);
    expect(ModuleB.getDataByGeneralUser).toHaveBeenCalledTimes(1);
  });
});

上記修正後のテストコードであればいずれのテストケースもパスするようになりました。

% jest ./module_a.test.ts

 PASS  test/module_a.test.ts
  module_a
    ✓ By Manager (2 ms)
    ✓ By GeneralUser

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        3.148 s
Ran all test suites matching /.\/module_a.test.ts/i.

おわりに

呼び出す関数をconditionalに変えるコードのJestによるテストでの注意点についてでした。

Jestにはテストを便利に実施するためのメソッド他にもまだまだたくさんあるので使いこなしていきたいです。

以上