JestではMock対象のModule(Function)の階層単位でtest fileを分けようという話

2022.05.18

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

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

今回は、JestではMock対象のModule(Function)の階層単位でtest fileを分けようという話です。

何が起こったのか

下記のようなhandler -> service -> infrastructureという3階層を構成するModuleがあります。(クリーンアーキテクチャ的な構成を想像してください)

handler.ts

import { service } from './service';

export const handler = () => {
  service();
};

service.ts

import { infrastructure } from './infrastracture';

export const service = (): string => {
  return infrastructure();
};

infrastructure.ts

export const infrastructure = (): string => {
  return 'Infrastructure Return Value';
};

このうちhandler.tsに対するテストを行いたく、次のような2つのテストを単一のfileに記述しました。1つはserviceをMockしたテスト、もう1つはinfrastructureをMockしたテストです。一見いずれのテストもPASSしそうな感じがしますね。

test/handler.test.ts

import { handler } from '../handler';
import { service } from '../service';
import { infrastructure } from '../infrastracture';

test('handler -> service', () => {
  //serviceをMockする
  (service as jest.Mock) = jest.fn();

  handler();

  expect(service).toBeCalledTimes(1);
});

test('handler -> service -> infrastructure', () => {
  //infrastructureをMockする
  (infrastructure as jest.Mock) = jest.fn();

  handler();

  expect(infrastructure).toBeCalledTimes(1);
});

しかしJestでテストを実行してみると、1つ目のテストのみPASSし、2つ目のテストはFAILしてしまいました。

npx jest test/handler.test.ts
 FAIL  test/handler.test.ts
  ✓ handler -> service (2 ms)
  ✕ handler -> service -> infrastructure (2 ms)

  ● handler -> service -> infrastructure

    expect(jest.fn()).toBeCalledTimes(expected)

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

      16 |   handler();
      17 |
    > 18 |   expect(infrastructure).toBeCalledTimes(1);
         |                          ^
      19 | });
      20 |

      at Object.<anonymous> (test/handler.test.ts:18:26)

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

原因

原因としては、1つ目のテストでMockしたserviceが、2つ目のテストでもMockされたままとなっていたためでした。そのため2つ目のテストでinfrastructureが1度も呼び出されずtoBeCalledTimes()による評価がFAILとなっていました。

試しに両テストの順番を入れ替えてみます。

test/handler.test.ts

//テストの順番を入れ替えた場合
import { handler } from '../handler';
import { service } from '../service';
import { infrastructure } from '../infrastracture';

test('handler -> service -> infrastructure', () => {
  (infrastructure as jest.Mock) = jest.fn();

  handler();

  expect(infrastructure).toBeCalledTimes(1);
});

test('handler -> service', () => {
  (service as jest.Mock) = jest.fn();

  handler();

  expect(service).toBeCalledTimes(1);
});

実行すると、両テストともPASSしました。

npx jest test/handler.test.ts
 PASS  test/handler.test.ts (5.41 s)
  ✓ handler -> service -> infrastructure (3 ms)
  ✓ handler -> service

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

しかしテストの記述の順番を意識しないといけないのは大変ですね。

Mock対象のModuleの階層単位にtest fileを分ける

「何かしらJestの機能を使えばなんとかなるだろう」とドキュメントを見てclearAllMocks()resetAllMocks()restoreAllMocks()などのそれっぽいMethodを調べたり試したりしてみましたが、解決はできるものは見つけられませんでした。

類似のIssueがありましたが、こちらでもtest fileを分ける以外の根本的な解決策は出てきていないようでした。

というわけで、対応として前述の両テストを2つのtest fileに分けてみます。

test/integration.test.ts

import { handler } from '../handler';
import { infrastructure } from '../infrastracture';

test('handler -> service -> infrastructure', () => {
  (infrastructure as jest.Mock) = jest.fn();

  handler();

  expect(infrastructure).toBeCalledTimes(1);
});

test/handler.test.ts

import { handler } from '../handler';
import { infrastructure } from '../infrastracture';

test('handler -> service -> infrastructure', () => {
  (infrastructure as jest.Mock) = jest.fn();

  handler();

  expect(infrastructure).toBeCalledTimes(1);
});

当然ですがテストの実行は両test fileともPASSしました。

npx jest test/handler.test.ts
 PASS  test/handler.test.ts (5.466 s)
  ✓ handler -> service (3 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        5.533 s, estimated 6 s
Ran all test suites matching /test\/handler.test.ts/i.

おわりに

JestではMock対象のModule(Function)の階層単位でtest fileを分けようという話でした。

この階層単位で分けるというのは、元々Moduleの階層が明確に分けられたクリーンアーキテクチャの構成を取っていれば難なく行うことができます。こういう部分でもクリーンアーキテクチャは活きてくるんですね。

以上