はじめに
こんにちは!株式会社エモーションテックで開発インターンをしている渋谷です! 今回はJestのモック関連で使用する
mockClear()
https://jestjs.io/ja/docs/mock-function-api#mockfnmockclearmockReset()
https://jestjs.io/ja/docs/mock-function-api#mockfnmockresetmockRestore()
https://jestjs.io/ja/docs/mock-function-api#mockfnmockreset
などの関数について実際のコード例と一緒にご紹介します!
※この記事は エモーションテック Advent Calendar 2022 22日目の記事です
1. mockClear()
Jestでテストを書く時にjest.fn()
やjest.spyOn()
を使ってスパイを作成することがあると思いますが、多くの場合リセットを考える必要はありません。
describe('saveLog', () => { it('create', async () => { const spy = jest.spyOn(jestMockService, 'log'); // スパイの作成 await jestMockController.saveLog({ id: 1, action: 'Create', }); expect(spy).toHaveBeenCalledTimes(1); // Success! }); it('update', async () => { const spy = jest.spyOn(jestMockService, 'log'); // スパイの作成 await jestMockController.saveLog({ id: 2, action: 'Update', }); expect(spy).toHaveBeenCalledTimes(1); // Success! }); });
このように各テストケースか、beforeEachで毎回spyOnすれば問題なくテストは通ります。
しかしスパイを最初に一度宣言する形にしたいときは注意が必要で、同じスパイを使用しているため呼ばれた回数などの情報がそのまま保存されてしまいます。
describe('saveLog', () => { const spy = jest.spyOn(jestMockService, 'log'); // スパイの作成 it('create', async () => { await jestMockController.saveLog({ id: 1, action: 'Create', }); expect(spy).toHaveBeenCalledTimes(1); // Success! }); it('update', async () => { await jestMockController.saveLog({ id: 2, action: 'Update', }); expect(spy).toHaveBeenCalledTimes(1); // Error, Received: number of calls: 2 }); });
これぐらい短いテストなら毎回spyを作成すればいいですし、そもそもテストコードで安易な共通化をするのは良くないということはありますが、実際のテストコードの中でこうすることもあるかと思います。
この場合はmockFnのメソッドであるmockClear()
を呼ぶことで呼ばれた回数などの情報を一度クリアすることができます。
上記の例ならafterEachで呼ぶことでテストが通るようになります。
describe('saveLog', () => { const spy = jest.spyOn(jestMockService, 'log'); afterEach(() => { spy.mockClear(); }); it('create', async () => { await jestMockController.saveLog({ id: 1, action: 'Create', }); expect(spy).toHaveBeenCalledTimes(1); // Success! }); it('update', async () => { await jestMockController.saveLog({ id: 2, action: 'Update', }); expect(spy).toHaveBeenCalledTimes(1); // Success! }); });
詳しく説明すると mockClear()
はjest.fn()
やjest.spyOn()
で作成されたモック関数mockFn
について、以下の4つの情報を削除します。
mockFn.mock.call
- モック関数に渡されたすべての引数の配列
const mockFn = jest.fn(); mockFn('arg1', 'arg2'); mockFn.mock.calls // [['arg1', 'arg2']]
mockFn.mock.instances
- モック関数のインスタンスの配列
const mockFn = jest.fn(); const initializedFn = new mockFn(); mockFn.mock.instances; // [initializedFn]
mockFn.mock.contexts
- モック関数のコンテクスト(
Function.prototype.apply
Function.prototype.bind
Function.prototype.call
のthisArg
)の配列
- モック関数のコンテクスト(
const mockFn = jest.fn(); const context = {} mockFn.apply(context, ['arg']); mockFn.mock.contexts; // [context]
mockFn.mock.results
- モック関数のから返却されたすべての結果の配列
const mockFn = jest.fn(); mockFn(); mockFn.mock.results // [{"type": "return", "value": undefined}]
2. mockReset()
次にjest.spyOn()
を使ってモックを作成する場合を考えてみます。
spyOnした関数から値を返すようにしたい場合は、mockImplementation()
mockReturnValue()
mockResolvedValue()
などが利用できます。
describe('getItems', () => { const mock = jest.spyOn(jestMockRepository, 'fetchLogs'); describe('create', () => { beforeEach(() => { mock.mockResolvedValue([{ id: 1, action: 'Create' }]); }); it('first-day', async () => { const results = await jestMockService.getItems(new Date('2022-01-01')); expect(results[0].logs[0]).toStrictEqual({ id: 1, action: 'Create' }); // Success! }); }); });
モックを複数のテストケース使う場合でも全て同じ値を返す分には問題ありませんし、全てのテストケースでモックの返り値を宣言すれば問題なく動きます。
しかし書き忘れた場合などに誤ったモックが使われることも考えられます。例えば以下のようにモックをした後、値がないことをテストしたい場合はエラーになってしまいます。
describe('getItems', () => { const mock = jest.spyOn(jestMockRepository, 'fetchLogs'); describe('create', () => { beforeEach(() => { mock.mockResolvedValue([{ id: 1, action: 'Create' }]); }); it('first-day', async () => { const results = await jestMockService.getItems(new Date('2022-01-01')); expect(results[0].logs[0]).toStrictEqual({ id: 1, action: 'Create' }); // Success! }); }); describe('returned no logs', () => { // fetchLogsから何も返ってこないケースをテストしたい it('rejects', async () => { await expect( jestMockService.getItems(new Date('2022-01-01')), ).rejects.toThrow(); // Error, Received promise resolved instead of rejected // [{ id: 1, action: 'Create' }]がfetchLogsから返却されるためテストが失敗する }); }); });
こういったモックの場合はmockClear()
を使ってもmockResolvedValue()
の情報はクリアされません。
なので、代わりにmockReset()
を使いましょう。
describe('getItems', () => { const mock = jest.spyOn(jestMockRepository, 'fetchLogs'); afterEach(() => { mock.mockReset(); }); describe('create', () => { beforeEach(() => { mock.mockResolvedValue([{ id: 1, action: 'Create' }]); }); it('first-day', async () => { const results = await jestMockService.getItems(new Date('2022-01-01')); expect(results[0].logs[0]).toStrictEqual({ id: 1, action: 'Create' }); // Success! }); }); describe('returned no logs', () => { // fetchLogsから何も返ってこないケースをテストしたい it('rejects', async () => { await expect( jestMockService.getItems(new Date('2022-01-01')), ).rejects.toThrow(); // Success! }); });
mockReset()
はmockClear()
でリセットするものに加え、mockImplemantation()
等の実装もリセットしてくれます。
3. mockRestore()
先ほどの例ではmockResolvedValue()
を使ってthenableな値を返していましたが、今度はmockRejectedValue()
を使って例外を返す場合を考えてみます。
異常系のテストの後に正常系のテストを実行しようとすると、すでに関数がモックされてしまっているためテストが通りません。
describe('gather', () => { describe('exception path', () => { // 例外をテストするためにエラーをモックから返却する const mock = jest .spyOn(jestMockRepository, 'collect') .mockRejectedValue(new Error('test')); it('fails when invalid id', async () => { await expect(jestMockService.gather('invalid id')).rejects.toThrow(); // Success! }); }); describe('happy path', () => { // 正常系のテストを行う it('succeeds when valid id', async () => { // モックされてしまっているためエラーになる const result = jestMockService.gather({ id: 101 }); // Error: test expect(result.id).toBe(101); expect(result.message).toBe('OK'); }); }); });
異常系のテストの後、mockReset()
を使用すれば、mockRejectedValue()
の実装はリセットされますが、リセットされた後は何も返さない関数になっているので同様にエラーになってしまいます。
もしテストファイルの中でモックされる前の関数の状態に戻したい時はmockRestore()
を使いましょう。
describe('gather', () => { describe('exception path', () => { // 例外をテストするためにエラーをモックから返却する const mock = jest .spyOn(jestMockRepository, 'collect') .mockRejectedValue(new Error('test')); it('fails when invalid id', async () => { await expect(jestMockService.gather('invalid id')).rejects.toThrow(); // Success }); // モックされていない状態に戻す afterAll(() => { mock.mockRestore(); }); }); describe('happy path', () => { // 正常系のテストを行う it('succeeds when valid id', async () => { const result = jestMockService.gather({ id: 101 }); expect(result.id).toBe(101); // Success! expect(result.message).toBe('OK'); // Success! }); });
ここで注意してほしいのが、mockRestore()
で元の状態に戻せるのはモック作成時にjest.spyOn()
を使った場合です。
jest.fn()
を使ってモックを作成した場合は戻せないので、モックを作成するときはspyOnを使うようにした方が無難かもしれません。
describe('mock restore', () => { it('spyOn', () => { const date = new Date('2022-06-30'); const mock = jest.spyOn(date, 'getDate').mockReturnValue(1); expect(date.getDate()).toBe(1); // Success! mock.mockRestore(); expect(date.getDate()).toBe(30); // Success! }); it('fn', () => { const date = new Date('2022-06-30'); const mockFn = jest.fn().mockReturnValue(1); date.getDate = mockFn; expect(date.getDate()).toBe(1); // Success! mockFn.mockRestore(); expect(date.getDate()).toBe(30); // Error, Received: undefined }); });
4. ファイル内の全てのモックをリセットする関数
テストを書くたびにafterEachで全部モックをリセットして...みたいなのが面倒な時は、jestオブジェクトから
jest.clearAllMocks()
https://jestjs.io/ja/docs/jest-object#jestclearallmocksjest.resetAllMocks()
https://jestjs.io/ja/docs/jest-object#jestresetallmocksjest.restoreAllMocks()
https://jestjs.io/ja/docs/jest-object#jestrestoreallmocks
これらのメソッドを使ってまとめてリセットできます。
これらの関数はそれぞれ全てのモック関数にmockClear()
mockReset()
mockRestore()
を書くのと同義です。
afterEach(() => { jest.clearAllMocks(); // テストファイル内のspyOnされている関数を全てmockClear()する });
5. 全てのテストケースでモックをリセットさせるjest設定
afterEachに書かずに全てリセットされるようにしたいという場合は、jestの設定ファイルで
clearMocks [boolean]
https://jestjs.io/ja/docs/configuration#clearmocks-booleanresetMocks [boolean]
https://jestjs.io/ja/docs/configuration#resetmocks-booleanrestoreMocks [boolean]
https://jestjs.io/ja/docs/configuration#resetmocks-boolean
を設定してしまうこともできます。全てデフォルト値はfalse
です。
この設定をtrue
にしておけば全てのテストケースの前に全てのモックをリセットしてくれるようになります。
package.json
{ ... "jest": { "clearMocks": true ... } }
おわりに
いかがでしたでしょうか? 今回ご紹介した関数や設定を使って、皆さんのテストがより良いものになれば幸いです! なお本記事に載っている情報はJest 29.3のものなので、バージョンによって内容と異なる可能性があることにご留意ください。
弊社ではNestJSを用いたバックエンド開発やAngularでのフロントエンド開発をしています! エンジニアの採用を積極的に行なっているので、ご興味のある方はぜひご応募ください!
参考文献
この記事は以下の情報を参考にしました。