[Jest] clearAllMocks()とresetAllMocks()の違いについて確認してみた

2022.02.11

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

JavaScriptのテスティングフレームワークJestでテストをする際に、テスト間のモックをクリアするためにclearAllMocks()をよく使用しています。しかし最近似たようなオブジェクトとしてresetAllMocks()なるものがあることを知りました。

今回は、clearAllMocks()resetAllMocks()の違いを確認してみました。

clearAllMocks()の動作

まずclearAllMocks()についてです。

Clears the mock.calls, mock.instances and mock.results properties of all mocks. Equivalent to calling .mockClear() on every mocked function.

実行すると、すべてのモックの mock.callsmock.instancesおよびmock.resultsをクリアしてくれるとのこと。

動作確認として、次のようなmodule_bを呼び出すmodule_aのテストを考えてみます。

src/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();
  }
};

src/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のテストです。afterEach()により各テスト(it())終了後にclearAllMocks()を実行するようにしています。

test/module_a.test.ts

import * as ModuleA from '../src/module_a';
import * as ModuleB from '../src/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> => {
    const res = await ModuleA.getDataByUser('manager');
    expect(res).toBe('dummy');
    //getDataByManagerが1回実行されることを期待
    expect(ModuleB.getDataByManager).toHaveBeenCalledTimes(1);
    expect(ModuleB.getDataByGeneralUser).toHaveBeenCalledTimes(0);
  });

  it('By Manager', async (): Promise<void> => {
    const res = await ModuleA.getDataByUser('manager');
    expect(res).toBe('dummy');
    //getDataByManagerが1回実行されることを期待
    expect(ModuleB.getDataByManager).toHaveBeenCalledTimes(1);
    expect(ModuleB.getDataByGeneralUser).toHaveBeenCalledTimes(0);
  });
});

jestを実行するとテストがPASSしました。

$  npx jest
 PASS  test/module_a.test.ts (8.73 s)
  module_a
    ✓ By Manager (2 ms)
    ✓ By GeneralUser (1 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        9.009 s
Ran all test suites.

テスト間でmock.callsがクリアされてtoHaveBeenCalledTimes()toHaveBeenCalledTimes()によるassertionが適切に行えています。これがclearAllMocks()を使うことによる嬉しみです。

resetAllMocks()

次にresetAllMocks()についてです。

Resets the state of all mocks. Equivalent to calling .mockReset() on every mocked function.

実行すると、すべてのモックのstateをクリア(初期化)してくれるとのこと。stateってなんなんでしょうね。

先程のテストのclearAllMocks()resetAllMocks()に置き換えて動作を見てみます。

test/module_a.test.ts

import * as ModuleA from '../src/module_a';
import * as ModuleB from '../src/module_b';

describe('module_a', () => {
  afterEach(() => {
    jest.resetAllMocks();
  });

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

  it('By Manager', async (): Promise<void> => {
    const res = await ModuleA.getDataByUser('manager');
    expect(res).toBe('dummy');
    //getDataByManagerが1回実行されることを期待
    expect(ModuleB.getDataByManager).toHaveBeenCalledTimes(1);
    expect(ModuleB.getDataByGeneralUser).toHaveBeenCalledTimes(0);
  });

  it('By Manager', async (): Promise<void> => {
    const res = await ModuleA.getDataByUser('manager');
    expect(res).toBe('dummy');
    //getDataByManagerが1回実行されることを期待
    expect(ModuleB.getDataByManager).toHaveBeenCalledTimes(1);
    expect(ModuleB.getDataByGeneralUser).toHaveBeenCalledTimes(0);
  });
});

するとテストがFAILしました。it()の外で記述しているmockReturnValue()によるモックが、1つ目のテストの後にリセットされてしまっているような動作ですね。

$  npx jest
 FAIL  test/module_a.test.ts (5.097 s)
  module_a
    ✓ By Manager (2 ms)
    ✕ By Manager (2 ms)

  ● module_a › By Manager

    expect(received).toBe(expected) // Object.is equality

    Expected: "dummy"
    Received: undefined

      22 |   it('By Manager', async (): Promise<void> => {
      23 |     const res = await ModuleA.getDataByUser('manager');
    > 24 |     expect(res).toBe('dummy');
         |                 ^
      25 |     //getDataByManagerが1回実行されることを期待
      26 |     expect(ModuleB.getDataByManager).toHaveBeenCalledTimes(1);
      27 |     expect(ModuleB.getDataByGeneralUser).toHaveBeenCalledTimes(0);

      at Object.<anonymous> (test/module_a.test.ts:24:17)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        5.139 s, estimated 6 s
Ran all test suites.

そこで、それぞれのit()内でmockReturnValue()によるモックをするようにすればテストはPASSしました。

test/module_a.test.ts

import * as ModuleA from '../src/module_a';
import * as ModuleB from '../src/module_b';

describe('module_a', () => {
  afterEach(() => {
    jest.resetAllMocks();
  });

  it('By Manager', async (): Promise<void> => {
    (ModuleB.getDataByManager as jest.Mock) = jest
      .fn()
      .mockReturnValue('dummy');
    (ModuleB.getDataByGeneralUser as jest.Mock) = jest
      .fn()
      .mockReturnValue('dummy');
    const res = await ModuleA.getDataByUser('manager');
    expect(res).toBe('dummy');
    //getDataByManagerが1回実行されることを期待
    expect(ModuleB.getDataByManager).toHaveBeenCalledTimes(1);
    expect(ModuleB.getDataByGeneralUser).toHaveBeenCalledTimes(0);
  });

  it('By Manager', async (): Promise<void> => {
    (ModuleB.getDataByManager as jest.Mock) = jest
      .fn()
      .mockReturnValue('dummy');
    (ModuleB.getDataByGeneralUser as jest.Mock) = jest
      .fn()
      .mockReturnValue('dummy');
    const res = await ModuleA.getDataByUser('manager');
    expect(res).toBe('dummy');
    //getDataByManagerが1回実行されることを期待
    expect(ModuleB.getDataByManager).toHaveBeenCalledTimes(1);
    expect(ModuleB.getDataByGeneralUser).toHaveBeenCalledTimes(0);
  });
});
$  npx jest
 PASS  test/module_a.test.ts
  module_a
    ✓ By Manager (2 ms)
    ✓ By Manager

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.73 s, estimated 6 s
Ran all test suites.

結局両者の違いは何なのか

結局両者の違いは何なのか、clearAllMocks()resetAllMocks()を個別のモックに対して実施するmockClear()mockReset()のドキュメントの記述を見てみると、ちゃんと具体的に書いてありました。

Clears all information stored in the mockFn.mock.calls, mockFn.mock.instances and mockFn.mock.results arrays. Often this is useful when you want to clean up a mocks usage data between two assertions.

Does everything that mockFn.mockClear() does, and also removes any mocked return values or implementations.

This is useful when you want to completely reset a mock back to its initial state. (Note that resetting a spy will result in a function with no return value).

記述の要点をまとめてみると、次のようになります。

  • mockClear()は、2つのassertion間でモックをクリーンアップしたい際に便利。
  • mockReset()は、mockClear()の実行内容に加えて、return values or implementationsを削除可能。
  • mockReset()は、モックのstateを完全にリセットしたい際に便利。

なるほど。大きな違いとしてはreturn values or implementationsが削除されるかどうか、のようですね。前節までの検証での動作にも合致します。

参考

以上