16
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

jest.fn / jest.spyOn / jest.mock と reset系の使い分け

16
Posted at

はじめに

NestJSでアプリを開発していて、jestを使ったmockを触る機会がありました。その際、mockの使い分けがわからなかったので、まとめてみました。

私が疑問に思ったのは「mockClear()をよく使っているけど、たまにでてくるmockReset()は何だろう?」とか「jest.mock()使わずに、importしてからmockすれば良いのでは?」みたいなところでした。この辺りも整理してみたらだいぶすっきりした気がします。

この記事では、Jestでメソッドをモックする代表的な3パターン(jest.fn / jest.spyOn / jest.mock)と、初期化(mockClear / mockReset / mockRestore)の使い分けを整理していきます。

1. 前提(サンプル)

以下のサンプルコードに対して書くテストコードとして、整理していきます。

// calculator.ts
export const calculator = {
  add(a: number, b: number) {
    return a + b;
  },
  sub(a: number, b: number) {
    return a - b;
  },
};
// calculator.spec.ts
import { calculator } from './calculator';

2. jest.fn: モック関数を「自分で作る」

jest.fn() は元の実装に紐づかない、ただのモック関数です。
DI(依存を引数やプロパティで渡す設計)と相性が良いです。

it('jest.fnの例', () => {
  const addMock = jest.fn().mockReturnValue(10);

  expect(addMock(1, 2)).toBe(10);
  expect(addMock).toHaveBeenCalledWith(1, 2);
});

オブジェクトに差し込むと「依存オブジェクトのモック」として使えます。

it('DIっぽく差し込む', () => {
  const addMock = jest.fn().mockReturnValue(10);
  const calcMock = { add: addMock };

  expect(calcMock.add(1, 2)).toBe(10);
});

初期化の定番

複数テストで同じ jest.fn() を使い回すなら、beforeEach でリセットします。

describe('jest.fnの初期化', () => {
  const addMock = jest.fn().mockReturnValue(10);

  beforeEach(() => {
    addMock.mockClear(); // 履歴だけ消す(実装は残る)
    // addMock.mockReset(); // 実装も消したいならこちら
  });

  it('test1', () => {
    addMock(1, 2);
    expect(addMock).toHaveBeenCalledTimes(1);
  });

  it('test2', () => {
    addMock(3, 4);
    expect(addMock).toHaveBeenCalledTimes(1);
  });
});

3. jest.spyOn: 既存オブジェクトのメソッドを差し替える

jest.spyOn(obj, 'method') は既存のメソッドをラップして監視/差し替えします。
テスト後に 元に戻せる(restoreできる) のが強みです。

it('spyOnの例', () => {
  const addSpy = jest.spyOn(calculator, 'add').mockReturnValue(999);

  expect(calculator.add(1, 2)).toBe(999);

  addSpy.mockRestore(); // 元の a+b に戻す
});

初期化の定番 (片付け含む)

前述の通り、mockRestore()を使って初期化をしていますが、spyOn()を使用したから必ずmockRestore()をしないといけないというわけではないようです。

describe('spyOnの初期化', () => {
  let addSpy: jest.SpiedFunction<typeof calculator.add>;

  beforeAll(() => {
    addSpy = jest.spyOn(calculator, 'add');
  });

  beforeEach(() => {
    addSpy.mockClear(); // 履歴だけ消す
  });

  afterAll(() => {
    addSpy.mockRestore(); // 元の実装に戻す(重要)
  });

  it('test1', () => {
    addSpy.mockReturnValue(10);
    expect(calculator.add(1, 2)).toBe(10);
  });

  it('test2', () => {
    addSpy.mockReturnValue(20);
    expect(calculator.add(3, 4)).toBe(20);
  });
});

4. reset系メソッドの違い

mockRest()mockRestore()は実装に影響を与えますが、mockClear()は呼び出し履歴を消すだけなので、テストではとりあえずafterEachallClearMocks()を入れているイメージがあります。

  • mockClear():履歴だけ消す(実装は残る)
  • mockReset():履歴+実装を消す(戻り値も消える)
  • mockRestore():元の実装に戻す(spyOn 用)
it('clear vs reset', () => {
  const fn = jest.fn().mockReturnValue(1);

  fn();
  fn();

  fn.mockClear();
  expect(fn()).toBe(1); // 実装は残る

  fn.mockReset();
  expect(fn()).toBeUndefined(); // 実装も消える
});

5. jest.mock: import されるモジュールを丸ごと差し替える

jest.mock('path', factory) はそのモジュールを import している全箇所に効くので、
「ネストの深いところで使われる関数を差し替えたい」時に便利です。

例:Service → AService → BUtil → checkTimeOverlap のように深い依存があっても、ちゃんとmockできます。

jest.mock('.../check-time-overlap.util', () => ({
  checkTimeOverlap: jest.fn(),
}));

import { checkTimeOverlap } from '.../check-time-overlap.util';

const mockedCheck = jest.mocked(checkTimeOverlap);

beforeEach(() => {
  mockedCheck.mockReset();
  mockedCheck.mockReturnValue(true);
});]

注意点

  • jest.mock は import 文字列が完全一致しないと当たりません
    (相対/絶対、aliasの揺れがあるとモックできない)
  • 可能なら import パスを統一(alias化)すると事故が減ります

おわりに

mockについてはざっくりとはわかりましたが、テストコードについては細かいところでわからないところだらけです。そのため、理解が進み次第また記事にまとめたいと思います!

最後に、mockのざっくり使い分けを書いておきます。

  • jest.fn
    • テスト側で依存(関数/オブジェクト)を組み立てて渡す(DI向き)
    • 元の実装に戻す概念がいらない時
  • jest.spyOn
    • 既存オブジェクトのメソッドを一部だけ差し替えたい
    • テスト後に元へ戻したい(mockRestore)
  • jest.mock
    • import されるモジュール自体を差し替えたい
    • ネストの深いところで呼ばれる util を確実にモックしたい
    • パス一致に注意

株式会社シンシア

株式会社シンシアでは、実務未経験のエンジニアの方や学生エンジニアインターンを採用し一緒に働いています。
※ シンシアにおける働き方の様子はこちら

弊社には年間100人程度の実務未経験の方に応募いただき、技術面接を実施しております。
この記事が少しでも学びになったという方は、ぜひ wantedly のストーリーもご覧いただけるととても嬉しいです!

16
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?