🔥

Jestでモックの作成方法について

に公開

はじめに

Jestの基礎はざっくり理解したので、公式ドキュメントに沿って次はモックについて学びたいと思いました。
Jestの基礎がある程度分かっている方が対象になります。

この記事で学べること

StripeのテストやAPIを実行できない場合、テスト環境の用意に時間が掛かってしまうなどでテストデータが用意できない場合にモックでテストデータを作成する方法について学べます。

動作環境

  • OS:macOS Sequoia 15.1.1
  • Node.js:22.6.0
  • npm:10.8.2
  • Jest:29.7.0

インストール方法

Jestの環境構築についてはこちらをご確認ください。

モックの基本的な使い方

まずはサンプルコードで動きを見てみる。
この関数は第一引数に配列を受け取り、第二引数に関数を受け取ります。
第二引数のコールバック関数を第一引数の配列の各要素に対して実行しています。

forEach.js
function forEach(items, callback) {
  for (const item of items) {
    callback(item);
  }
}
module.exports = forEach;

jest.fn()モック関数(ダミー関数)を作成することができる。
今回の場合は、forEach関数の第二引数に渡すコールバック関数をモック関数で作成している。

forEach.test.js
const forEach = require("./forEach");
const mockCallback = jest.fn((x) => 42 + x);

test("forEach mock function", () => {
  // モック関数を渡して実行
  forEach([0, 1], mockCallback);
  console.log(mockCallback.mock);

  // モック関数が2回呼び出されたことを確認
  expect(mockCallback.mock.calls).toHaveLength(2);

  // 最初の呼び出しの最初の引数が0であることを確認
  expect(mockCallback.mock.calls[0][0]).toBe(0);

  // 2回目の呼び出しの最初の引数が1であることを確認
  expect(mockCallback.mock.calls[1][0]).toBe(1);

  // 最初の呼び出しの戻り値が42であることを確認
  expect(mockCallback.mock.results[0].value).toBe(42);
});

このようにモック関数の.mockプロパティを使用する事で様々なテストが出来ることが分かります。
.mockプロパティにはモック関数呼び出し時のデータと、関数の返り値が記録されているみたいなので、ログで確認してみる。

> test
> jest forEach.test.js

  console.log
    {
      calls: [ [ 0 ], [ 1 ] ],
      contexts: [ undefined, undefined ],
      instances: [ undefined, undefined ],
      invocationCallOrder: [ 1, 2 ],
      results: [ { type: 'return', value: 42 }, { type: 'return', value: 43 } ],
      lastCall: [ 1 ]
    }

      at Object.log (forEach.test.js:12:11)

ログで.mockプロパティを確認する事で、どのデータを使ってテストしているのかを確認できます。
callsでは呼び出し時の引数の値がそれぞれ入った配列になっています。
resultsにはオブジェクトが入り、typeにはモック関数が正常に値を返したときreturnvalueが入り、モック関数がエラーを投げたときthrowvalueになり、valueには実際に投げられたエラーオブジェクトが入ります。

モック関数の呼び出し履歴を詳しく確認する

contextsの使い方

contextsは関数がどのthisを使って呼び出されたかを確認したいときに使います。
JSのthisについてはこちらの記事がすごく分かりやすいです。

// モック関数
const mockFn = jest.fn(function () {
  return this.name;
});

// テスト対象のオブジェクト
const obj1 = { name: "Taro", call: mockFn };
const obj2 = { name: "Hanako", call: mockFn };

// テスト対象のオブジェクトのメソッドを呼び出す
obj1.call(); // thisはobj1
obj2.call(); // thisはobj2

// モック関数の呼び出し結果を確認
test("contextsを使ってthisを確認", () => {
  expect(mockFn.mock.contexts[0].name).toBe("Taro");
  expect(mockFn.mock.contexts[1].name).toBe("Hanako");
});

このように.mock.contextsを使えば、モック関数が「どのオブジェクトのthisで呼ばれたか」を正確に検証できます。

instancesの使い方

instancesはインスタンス化された回数とインスタンスそのものを確認できる。

// モック関数
const MockClass = jest.fn(function (name) {
  this.name = name;
});

// テスト対象のオブジェクト
new MockClass("Taro");
new MockClass("Hanako");

test("instancesでnewされたインスタンスのプロパティ確認", () => {
  expect(MockClass.mock.instances.length).toBe(2);
  expect(MockClass.mock.instances[0].name).toBe("Taro");
  expect(MockClass.mock.instances[1].name).toBe("Hanako");
});

このように.mock.instancesを使えば、インスタンス化された回数や、それぞれのインスタンスが持つプロパティの値を確認できます。

invocationCallOrderの使い方

invocationCallOrderは、モック関数が呼び出された順序を追跡する配列。

// モック関数
const fn1 = jest.fn();
const fn2 = jest.fn();

fn1(); // 1回目
fn2(); // 1回目
fn1(); // 2回目

test("呼び出し順の確認", () => {
  // fn1がfn2よりも先に呼び出される事を確認するテスト
  expect(fn1.mock.invocationCallOrder[0]).toBeLessThan(
    fn2.mock.invocationCallOrder[0]
  );
  // fn2がfn1よりも先に呼び出される事を確認するテスト
  expect(fn2.mock.invocationCallOrder[0]).toBeLessThan(
    fn1.mock.invocationCallOrder[1]
  );
});

非同期処理で順序が大事な場合や、複数のAPI呼び出しの順序を検証する際に使用します。

lastCallの使い方

lastCallは、最後に呼び出された時の引数(配列)。

// モック関数
const mockFn = jest.fn();

mockFn("first");
mockFn("second");

test("lastCallで最後の引数を確認", () => {
  expect(mockFn.mock.lastCall[0]).toBe("second");
});

関数が最後に呼ばれた際の引数を返すだけなので分かりやすいかと思います。

APIからデータを取得するメソッドのモックを作成する

ユーザー情報を返すAPIで何らかの理由によりAPIが使用出来ないと仮定してモックを作成してみる

fetchUser.js
const axios = require("axios");
async function fetchUser(userId) {
  const response = await axios.get(`https://api.example.com/users/${userId}`);
  return response.data;
}
module.exports = fetchUser;
fetchUser.test.js
// axiosをモック化する
jest.mock("axios");

test("APIが使えないと仮定してモックでユーザー情報を返す", async () => {
  // モックのレスポンスデータ
  const mockUser = { id: 1, name: "Taro" };

  // axios.getが呼ばれたらモックデータを返すように設定
  axios.get.mockResolvedValue({ data: mockUser });

  // mockUserのオブジェクトを返す
  const user = await fetchUser(1);

  // モックデータが返されることを確認
  expect(user).toEqual(mockUser);

  // fetchUser関数のaxios.getが呼ばれたことを確認
  // axios.getの引数が正しいことを確認
  expect(axios.get).toHaveBeenCalledWith("https://api.example.com/users/1");
});

実際にコードを書いていて色々な理由があり、APIが実行できない場合等はあるかと思いますが、モックを作成する事で擬似的にデータを作成しテストが出来るのは凄く便利だと思います。

まとめ

今回はモックの作成方法について学びました。
これまで環境依存によってAPIが実行できない場合などに、実装したコードが問題ないのかテストできない事が多くありました。
その場合はコードの確認のみで済ましてしまう事も多かったのですが、慣れればテストデータを用意する事も結構簡単なので今後はモックも活用して単体テストを積極的に書いていこうと思います。

参考資料

Discussion