モック関数
モック関数によりコード間の繋がりをテストすることができます。 関数が持つ実際の実装を除去したり、関数の呼び出し(また、呼び出しに渡されたパラメータも含め)をキャプチャしたり、new
によるコンストラクタ関数のインスタンス化をキャプチャできます。 そうすることでテスト時のみの返り値の設定をすることが可能になります。
関数をモックするには、次の2つの方法があります。 1つは、テストコードの中でモック関数を作成するという方法。 もう1つは、manual mock
を作成してモジュールの依存性を上書きするという方法です。
モック関数を利用する
forEach
関数の実装をテストすることを考えてみましょう。 この関数は、与えられた配列の各要素に対して、コールバック関数を呼び出します。
export function forEach(items, callback) {
for (const item of items) {
callback(item);
}
}
この関数をテストするために、モック関数を利用して、コールバックが期待通り呼び出されるかを確認するためにモックの状態を検証することができます。
const forEach = require('./forEach');
const mockCallback = jest.fn(x => 42 + x);
test('forEach mock function', () => {
forEach([0, 1], mockCallback);
// The mock function was called twice
expect(mockCallback.mock.calls).toHaveLength(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);
});
.mock
プロパティ
すべてのモック関数には、この特別な .mock
プロパティがあり、モック関数呼び出し時のデータと、関数の返り値が記録されています。 .mock
プロパティには、各呼び出し時の this
の値も記録されているため、this
の値のチェックも可能です。
const myMock1 = jest.fn();
const a = new myMock1();
console.log(myMock1.mock.instances);
// > [ <a> ]
const myMock2 = jest.fn();
const b = {};
const bound = myMock2.bind(b);
bound();
console.log(myMock2.mock.contexts);
// > [ <b> ]
以下のモックのプロパティを使用すると、関数がどのように呼び出され、どのようにインスタンス化され、返り値が何であったのかを確認することができます。
// The function was called exactly once
expect(someMockFunction.mock.calls).toHaveLength(1);
// The first arg of the first call to the function was 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
// The second arg of the first call to the function was 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');
// The return value of the first call to the function was 'return value'
expect(someMockFunction.mock.results[0].value).toBe('return value');
// The function was called with a certain `this` context: the `element` object.
expect(someMockFunction.mock.contexts[0]).toBe(element);
// This function was instantiated exactly twice
expect(someMockFunction.mock.instances.length).toBe(2);
// The object returned by the first instantiation of this function
// had a `name` property whose value was set to 'test'
expect(someMockFunction.mock.instances[0].name).toBe('test');
// The first argument of the last call to the function was 'test'
expect(someMockFunction.mock.lastCall[0]).toBe('test');
モックの戻り値
モック関数は、テスト中のコードにテスト用の値を注入するのにも利用できます。
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
モック関数は、関数的な継続渡し (continuation-passing) のスタイルを利用したコードでも、とても効果的です。 コードをこのスタイルで書くことで、本物のコンポーネントの振る舞いを再現するような複雑なスタブが必要になることを避けることができ、テストで使われる直前に値を直接注入するができるようになります。
const filterTestFn = jest.fn();
// Make the mock return `true` for the first call,
// and `false` for the second call
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);
const result = [11, 12].filter(num => filterTestFn(num));
console.log(result);
// > [11]
console.log(filterTestFn.mock.calls[0][0]); // 11
console.log(filterTestFn.mock.calls[1][0]); // 12
実世界のほとんどの例では、依存しているコンポーネントのモック関数を見つけ出して構成することが必要となりますが、テクニック自体は一緒です。 こうしたテストを書く場合は、関数の内の直接テストされていないロジックを実装したくなる誘惑を避けるように努めましょう。
モジュールのモック
API からユーザーを取得するクラスがあるとします。 以下のクラスは、axios を使用して API を呼び、全てのユーザーが持っている data
属性を返します。
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
さて、このメソッドを実際に API にアクセスせずにテストするために (もしそのようなテストを作れば、遅くて壊れやすいテストになってしまいます)、jest.mock(...)
関数を使えば、axios モジュールを自動的にモックすることができます。
一度モジュールをモックすれば、.get
に対して mockResolvedValue
メソッドを使えるようになり、テストで検証したいデータを返させるようにできます。 実装上は、 axios.get('/users.json')
に偽のレスポンスを返すようにさせています。
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));
});
部分的なモック
モジュールを部分的にモックすることが可能です。残りの部分は実際の実装そのままになります。
export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';
//test.js
import defaultExport, {bar, foo} from '../foo-bar-baz';
jest.mock('../foo-bar-baz', () => {
const originalModule = jest.requireActual('../foo-bar-baz');
//Mock the default export and named export 'foo'
return {
__esModule: true,
...originalModule,
default: jest.fn(() => 'mocked baz'),
foo: 'mocked foo',
};
});
test('should do a partial mock', () => {
const defaultExportResult = defaultExport();
expect(defaultExportResult).toBe('mocked baz');
expect(defaultExport).toHaveBeenCalled();
expect(foo).toBe('mocked foo');
expect(bar()).toBe('bar');
});
モックの実装
とはいえ、指定された値を返すという能力を越えて完全に実装をモック化することが便利なケースがあります。 これはjest.fn
またはモック関数の mockImplementationOnce
メソッドを利用することで実現できます。
const myMockFn = jest.fn(cb => cb(null, true));
myMockFn((err, val) => console.log(val));
// > true
mockImplementation
メソッドは他のモジュールによって作成されたモック関数のデフォルトの実装を定義したいときに便利です。
module.exports = function () {
// some implementation;
};
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');
// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42
関数への複数回への呼び出しで異なる結果を得るように複雑な挙動をするモック関数を再作成する必要がある場合はmockImplementationOnce
メソッドを使用して下さい。
const myMockFn = jest
.fn()
.mockImplementationOnce(cb => cb(null, true))
.mockImplementationOnce(cb => cb(null, false));
myMockFn((err, val) => console.log(val));
// > true
myMockFn((err, val) => console.log(val));
// > false
モック関数がmockImplementationOnce
によって定義された実装が全て使い切った時は、 (もし定義されていれば) jest.fn
のデフォルトの実装を実行します。
const myMockFn = jest
.fn(() => 'default')
.mockImplementationOnce(() => 'first call')
.mockImplementationOnce(() => 'second call');
console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'
よくチェーンされる(そしてのために常に this
を返す必要のある)メソッ ドがあるケースのために、この実装を単純化する糖衣APIを.mockReturnThis()
の形で全てのモックが備えています。
const myObj = {
myMethod: jest.fn().mockReturnThis(),
};
// is the same as
const otherObj = {
myMethod: jest.fn(function () {
return this;
}),
};
モック名
You can optionally provide a name for your mock functions, which will be displayed instead of 'jest.fn()'
in the test error output. Use .mockName()
if you want to be able to quickly identify the mock function reporting an error in your test output.
const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockImplementation(scalar => 42 + scalar)
.mockName('add42');
カスタムマッチャ
最後にモック関数がどのように呼ばれたかを検査する必要を減らすため、いくつかのカスタムマッチャを用意しておきました。
// The mock function was called at least once
expect(mockFunc).toHaveBeenCalled();
// The mock function was called at least once with the specified args
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);
// The last call to the mock function was called with the specified args
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);
// All calls and the name of the mock is written as a snapshot
expect(mockFunc).toMatchSnapshot();
これらのマッチャは .mock
プロパティを検査する一般的な方法の糖衣構文です。 より好みに合うものが欲しい場合や、より特定のテストに向けたものが必要な場合は、いつでも手動でカスタムマッチャを追加することができます。
// The mock function was called at least once
expect(mockFunc.mock.calls.length).toBeGreaterThan(0);
// The mock function was called at least once with the specified args
expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);
// The last call to the mock function was called with the specified args
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
arg1,
arg2,
]);
// The first arg of the last call to the mock function was `42`
// (note that there is no sugar helper for this specific of an assertion)
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);
// A snapshot will check that a mock was invoked the same number of times,
// in the same order, with the same arguments. It will also assert on the name.
expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
expect(mockFunc.getMockName()).toBe('a mock name');
マッチャーの完全なリストについては、 リファレンスドキュメントを確認してください。