モック関数
モック関数によりコード間の繋がりをテストすることができます。 関数が持つ実際の実装を除去したり、関数の呼び出し(また、呼び出しに渡されたパラメータも含め)をキャプチャしたり、new
によるコンストラクタ関数のインスタンス化をキャプチャできます。 そうすることでテスト時のみの返り値の設定をすることが可能になります。
関数をモックするには、次の2つの方法があります。 1つは、テストコードの中でモック関数を作成するという方法。 もう1つは、manual mock
を作成してモジュールの依存性を上書きするという方法です。
モック関数を利用する
forEach
関数の実装をテストすることを考えてみま しょう。 この関数は、与えられた配列の各要素に対して、コールバック関数を呼び出します。
export function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
この関数をテストするために、モック関数を利用して、コールバックが期待通り呼び出される かを確認するためにモックの状態を検証することができます。
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