Closed8

jest mock 調査まとめ

ハトすけハトすけ

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 に対して mockReturnValuemockReturnValueOnceメソッドを呼び出す。

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

決められた値ではなく、実装自体を渡すことも可能。mockImplementationmockImplementationOnceを利用する。

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() パターン

ドキュメントに書いてある方法

user.js
import axios from 'axios';

class Users {
  static all() {
    return axios.get('/users.json').then(resp => resp.data);
  }
}

export default Users;
users.test.js
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を参考にしてください。
https://stackoverflow.com/questions/59035729/how-to-mock-named-exports-in-jest-with-typescript

hoge.ts
export const hogeFunction = () => 'hogehoge';
user.ts
import {hogeFunction} from './hoge';

class Users {
  static all() {
    return hogeFunction();
  }
}

export default Users;
users.test.ts
import Users from './users';

jest.mock('./hoge', () => {
  return {
    hogeFunction: () => 'fuga',
  };
});

test('should fetch fuga', () => {
  return expect(Users.all()).toBe('fuga');
});

TypeScriptでnamed exportし、かつそのメソッド呼び出しをチェックする

hoge.ts
export const hogeFunction = (repeatNum: number): string =>
  'hoge'.repeat(repeatNum);
user.ts
import { hogeFunction } from './hoge';

class Users {
  static all() {
    return hogeFunction(2);
  }
}

export default Users;

users.test.ts
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ファイル以外は同じ実装です。

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 で定義しないとうまく動きません。注意してください。

このスクラップは2021/12/01にクローズされました