モック

モック機能を使うと、

  • モジュール外機能を簡略化
  • 関数は何と共に何回呼ばれたか(spy

ができます。

モジュール外機能を簡略化

例えばあるクラスAには、内部で API を通してデータを保存・取得するメソッドを持っていたとします。API には多分何かしらの制約を持ってるのではと思います。例えばこんな感じです。

  • あるフィールドはユニークな値しか保存できない
  • レスポンスにランダムなidを返す
  • 取得にはランダムなidを使う

このような時に実際に API 叩いてしまうと、決められた順番に実行しなければパスできない実質1つの巨大なテストケースになってしまいます。後処理が必要なものだったりするともっと大変。
API の周りの機能は完全にモジュール外の要素です。なのに実際に API まで叩かないといけないのか。
特にユニットテストレベルではその辺までしっかりする必要性は薄いです。なので、この辺(+α)を以下のような決めた値を返すだけのスタブに変えてしまいます。

  • 保存できる
  • レスポンスにidbyMVcとして返す
  • idが何でも取得できる
  • まとめて取得した場合は2件データが入っている

Jest にはこのような既存のモジュールをモック化するいくつかの機能を持ってます。

jest.mock

もしあるモジュールがエクスポートしてる機能をまとめてモック化したいならjest.mock(path)を使います。これは関数はもちろんオブジェクトが持ってるメソッドなどをすべてundefinedを返すモック(spy)に書き換えます。

例えば開発しているモジュールmy-client.jsが以下のような実装だとします。

const fetch = require('cross-fetch');

module.exports = class MyClient {
  constructor(token) {
    this.token = token;
  }
  
  /**
   * データ取得
   * @returns {Promise<Object[]>}
   */
  get() {
    return fetch(/* url */'...').then(res => res.json());
  }
  
  /**
   * データ保存
   * @returns {Promise<number>} 保存したデータの ID
   */
  save(data) {
    return fetch(/* url */'...', {method: 'POST'}).then(/* ... */).then(process);
  }
}

また、テストファイルでは以下のようにこのモジュールを使ってるとします。

const MyClient = require("./my-client");

test("get", async () => {
  const client = new MyClient("...");

  expect(client.get()).resolves.toMatchObject([
    { id: "a" },
    { id: "b" }
  ]);
});

my-clientcross-fetchモジュールに依存しており、今の所getテストでは実際にcross-fetchを通してリクエストを飛ばしてしまいます。そしてレスポンスデータが2件の時だけパスできます。もしこの後保存 API を飛ばすと API 仕様によっては、このテストは失敗するようになるかもしれません。

ここでcross-fetchをモック化して結果的に必ず[{id: 'a'}, {id: 'b'}]を返す関数にしてしまいます。それには以下のような行を挟みます。

const fetch = require('cross-fetch');
jest.mock('cross-fetch');

fetch.mockResolvedValueOnce({
  json: () => [{ id: "a" }, { id: "b" }]
});

mockResolvedValueOnce(value)は1度だけPromise.resolve(value)を返すモック関数化できます。my-clientの中のget関数は、単に取得したデータを返すだけなので、これで常にgetテストが通るようになります。

上記のテストではテストファイルで直接モックを作りました。しかし中にはいくつものモック関数をテストファイルの中で作る必要があったり、何度も同じことを書く必要があったりで使いまわしたい場面がでてくるかもしれません。
そのような場合にはマニュアルモックが使えます。

マニュアルモック

マニュアルモックは、テストファイルと同じ階層に__mocks__/cross-fetch.jsのように__mocks__/の中にモジュール名ファイルを入れることで、jest.mock('cross-fetch')時にそのファイルに書かれた内容でモック化してくれる機能です。

例えば先程のcross-fetchと同様のマニュアルモックを作ると以下のようになります。

const fetch = jest.genMockFromModule("cross-fetch");

fetch.mockResolvedValueOnce({
  json: () => [{ id: "a" }, { id: "b" }]
});

module.exports = fetch;

これからはjest.mock('cross-fetch')とするとfetchした時にPromise.resolve([{ id: "a" }, { id: "b" }])が返されるようになりました。

モック関数

先程出てきたmockResolvedValueOnce(value)は「1度だけPromise.resolve(value)を返す」という関数にするものでしたが、他にも色々あります。

mockResolvedValue(value)は1度じゃなく常にPromise.resolve(value)を返すようにします。同じようにmockRejectedValue,mockRejectedValueOncemockReturnValuemockReturnValueOnceがあり、それぞれPromise.reject(value)value版です。
また完全に自分で実装するmockImplementation(fn)mockImplementationOnce(fn)もあります。引数のfnは単なる関数で、モック関数が呼ばれた時に単にそれが呼ばれ結果が戻り値で返ります。

モックは受け取った引数や戻り値を覚えています。それらはそれぞれmock.callsmock.resultsを通して取得できます。例えばmock.calls[0][1]で1回目実行時の第2引数、mock.results[0]で1回目実行時の戻り値のような感じです。

上記のmock.callsmock.resultsの履歴を消したい時はmockResetを実行します。また、mockClearを呼ぶとモックをjest.fn()直後の状態に戻せます。