jest mock 調査まとめ
jestのモックまわりが地味にむずい。とくにモジュールのモック。
jestのドキュントはこちら。https://jestjs.io/ja/docs/mock-functions
以下ドキュメントの自分なりの整理。
jestのモックの基本 モック関数と引数や呼ばれた回数の記録(sinonで言うところのspy機能)
基本はモック関数であるjest.fn
を利用する。
jest.fn() の返り値自体にmock
というプロパティがあり、それにアクセスすることで、何回呼ばれたとか、引数の値はなんだったのかとかわかる。
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);
// The mock function is called twice
expect(mockCallback.mock.calls.length).toBe(2);
// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42);
これらを使いやすくしたカスタムマッチャも存在する。
// The mock function was called at least once
expect(mockFunc).toHaveBeenCalled();
// 同義: expect(mockFunc.mock.calls.length).toBeGreaterThan(0);
// The mock function was called at least once with the specified args
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);
// 同義: expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);
// The last call to the mock function was called with the specified args
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);
// 同義: expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([ arg1, arg2 ]);
// All calls and the name of the mock is written as a snapshot
expect(mockFunc).toMatchSnapshot();
// 同義:
// expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
// expect(mockFunc.getMockName()).toBe('a mock name');
jestのモックの基本 あらかじめモック関数が返す値を決めておく
先程の jest.fn
に対して mockReturnValue
やmockReturnValueOnce
メソッドを呼び出す。
const myMock = jest.fn();
console.log(myMock());
// > undefined
myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true
決められた値ではなく、実装自体を渡すことも可能。mockImplementation
やmockImplementationOnce
を利用する。
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');
// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42
const myMockFn = jest
.fn()
.mockImplementationOnce(cb => cb(null, true)) // 1回目返り値
.mockImplementationOnce(cb => cb(null, false)); // 2回目返り値
myMockFn((err, val) => console.log(val));
// > true
myMockFn((err, val) => console.log(val));
// > false
jestの基本 モジュールをモックする(sinonで言うところのstub機能)
jestでモジュールをモックする方法は大きく2つあります。
- テストの最初に
jest.mock
メソッドを利用する。 - テストの適当な場所で
jest.spyOn()
メソッドを利用する。
jest.mock
は、たとえdescribeないのbeforeで定義しても、テストファイルの冒頭で実行されます。またmockする実装を定義してあげなければ、モジュールは何もしません。
jest.spyOn
は、jest.mock
と違い、mockする実装を定義してあげなければ、本物のモジュールの実装が利用されます。
jest.mock()
パターン
ドキュメントに書いてある方法
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
import axios from 'axios';
import Users from './users';
jest.mock('axios');
test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);
// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))
return Users.all().then(data => expect(data).toEqual(users));
});
注意点
- TypeScriptだと
mockResolvedValue
でtypeエラーがでる(as jest.Mockでキャストすればいけそう?)。 - named exportで直接関数返されるとどう実装すればいいかわかない
TypeScriptでnamed exportの方法
こちらのstack overflowを参考にしてください。
export const hogeFunction = () => 'hogehoge';
import {hogeFunction} from './hoge';
class Users {
static all() {
return hogeFunction();
}
}
export default Users;
import Users from './users';
jest.mock('./hoge', () => {
return {
hogeFunction: () => 'fuga',
};
});
test('should fetch fuga', () => {
return expect(Users.all()).toBe('fuga');
});
TypeScriptでnamed exportし、かつそのメソッド呼び出しをチェックする
export const hogeFunction = (repeatNum: number): string =>
'hoge'.repeat(repeatNum);
import { hogeFunction } from './hoge';
class Users {
static all() {
return hogeFunction(2);
}
}
export default Users;
import * as defaultHogeFunction from './hoge';
import Users from './users';
jest.mock('./hoge', () => {
return {
// ここで`mockReturnValue`を定義してもなぜか有効にならない。 NOTE: https://stackoverflow.com/questions/46431638/jest-fn-return-value-returns-undefined-when-using-jest-mock
hogeFunction: jest.fn(),
};
});
test('should fetch fuga', () => {
(defaultHogeFunction.hogeFunction as jest.Mock).mockReturnValue('fuga');
expect(Users.all()).toBe('fuga');
expect(defaultHogeFunction.hogeFunction).toHaveBeenCalledWith(2);
});
ポイントは
-
import * as 適当な名前 from './hoge'
とnamed exportをまとめること -
jest.mock
のなかで、関数を改めて定義しなおすこと - その際に、
jest.fn
を利用することで、あとのコードで引数呼び出しチェックができること
jest.spyOn()
パターン
ぼくはどちらかというとこちらが好きですね。
users.test.ts
ファイル以外は同じ実装です。
import * as defaultHogeFunction from './hoge';
import Users from './users';
let hogeFunctionSpy: jest.SpyInstance<unknown>;
test('should fetch fuga', () => {
hogeFunctionSpy = jest
.spyOn(defaultHogeFunction, 'hogeFunction')
.mockReturnValue('fuga');
expect(Users.all()).toBe('fuga');
expect(hogeFunctionSpy).toHaveBeenCalledWith(2);
});
個人的に、reactにおけるカスタムフックなどは、関数自体をnamed exportすることが多いので、そのときはspyOn
パターンで行こうと思ってます。
注意
create-react-appを使っている方へ
どうやら、create-react-appはデフォルトで各テスト後にmockのリセットを行っているみたいで、spyのmockReturnValueを定義したいとき beforeAll
ではなく beforeEach
で定義しないとうまく動きません。注意してください。