モック関数
モック関数は、関数の実際の実装を削除、関数呼び出し(および、その呼び出しで受け渡されたパラメータ)をキャプチャ、新しいインスタンスでインスタンス化された場合のコンストラクタ関数のインスタンスをキャプチャし、テスト時に戻り値を構成することで、コード間のリンクをテストできます。
モック関数を作成するには2つの方法があります。1つは、テストコードで使用するためのモック関数を作成する方法、もう1つは、モジュール依存関係を上書きするためのマニュアルモック
を作成する方法です。
モック関数の使用
提供された配列の各項目のコールバックを呼び出す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
の値も追跡するため、それも検査可能です。
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
モック関数は、関数継続的なやり方でコードを使用する場合にも非常に効果的です。このスタイルで書かれたコードは、複雑なスタブを作成して置き換える実際のコンポーネントの動作を再現するよりも、使用される直前に値をテストに直接投入します。
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
を返す必要がある)場合、すべてのモックにある.mockReturnThis()
関数の形式でこれを簡素化する甘美なAPIがあります
const myObj = {
myMethod: jest.fn().mockReturnThis(),
};
// is the same as
const otherObj = {
myMethod: jest.fn(function () {
return this;
}),
};
モック名
オプションで、モック関数に名前を付けることができます。名前はテストエラー出力で'jest.fn()'
の代わりに表示されます。テスト出力でエラーを報告しているモック関数をすばやく特定できるようにする場合は、.mockName()
を使用します。
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');
完全なマッチャリストについては、リファレンスドキュメントをご覧ください。