はじめに
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()は呼び出し履歴を消すだけなので、テストではとりあえずafterEachにallClearMocks()を入れているイメージがあります。
-
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 のストーリーもご覧いただけるととても嬉しいです!