📖

Jestでmock, stubする

2024/01/23に公開

Jestでのmockとstub

Jestではmockとstubで関数は区別されておらず、jest.fn, jest.mock, jest.spyOnを使います。

emailを送信するという振る舞いをmockしてテストする

SendGridというメールサービスのライブラリを使って、以下のようなプロダクションコードがあるとします。

import { send } from "./lib/send-email";

export const sendEmail = async ({ to }: { to: string }) => {
  await send({
    to,
    from: "from@example.com",
    subject: "タイトル",
    text: "あいうえお",
  });
};
lib/send-email.ts
import { MailService } from "@sendgrid/mail";

export const send = async ({
  to,
  from,
  subject,
  text,
}: {
  to: string;
  from: string;
  subject: string;
  text: string;
}) => {
  const mailService = new MailService();
  mailService.setApiKey("SG.example");
  const msg = {
    to,
    from,
    subject,
    text,
  };
  await mailService.send(msg);
};

通常だとmailService.send(msg)が外部へのリクエストを発生させるので、発生させない形でテストしたいかと思います。
外部リクエストを伴うテストは、ネットワークの問題、外部サービスのダウンタイム、APIのレート制限などにより不安定になる可能性があったり、料金がかかったり、これらの外部要因に依存したくないためです。
あと単純に、テスト時間を短くしたいと理由もあります。

mailService.send(msg)をmockしてテストしたいと思います。

jest.spyOn()を使う場合

今回はMailServiceクラスの特定のメソッドだけmockしたいので以下に倣います。
https://jestjs.io/ja/docs/es6-class-mocks#mocking-a-specific-method-of-a-class

import * as sendEmailLib from "../app/lib/send-email";
import { sendEmail } from "../app/example-serivce";
import { MailService } from "@sendgrid/mail";

describe("email送信", () => {
  test("emailが送信される", async () => {
    // const sendSpy = jest.spyOn(MailService.prototype, "send")のみだと、
    // send()の中身が実行されてしまいます
    const sendSpy = jest
      .spyOn(MailService.prototype, "send")
      // send()のreturnの型に合わせる必要がある
      .mockResolvedValue([{} as any, {}]);

    await sendEmail({ to: "to@example.com" });

    expect(sendSpy).toHaveBeenCalledWith({
      to: "to@example.com",
      from: "from@example.com",
      subject: "タイトル",
      text: "あいうえお",
    });
  });
});

jest.mock()を使う場合

jest.mockの場合はファイルのトップレベルで呼び出す必要があります。
https://archive.jestjs.io/docs/ja/es6-class-mocks#自動モック
https://archive.jestjs.io/docs/ja/mock-function-api#jestmockedclass
を参考に以下のようになります。

jest.mock("@sendgrid/mail");
import { sendEmail } from "../app/example-serivce";
import { MailService } from "@sendgrid/mail";
const MailServiceMock = MailService as jest.MockedClass<typeof MailService>;

describe("email送信", () => {
  test("emailが送信されるmock", async () => {
    await sendEmail({ to: "to@example.com" });

    expect(MailServiceMock.mock.instances[0].send).toHaveBeenCalledWith({
      to: "to@example.com",
      from: "from@example.com",
      subject: "タイトル",
      text: "あいうえお",
    });
  });
});

TypeScriptで型のエラーが出ないように

const MailServiceMock = MailService as jest.MockedClass<typeof MailService>;

としておく必要があります。

外部APIからのデータの取得をstubしてテストする

次にstubを考えます。
以下のように外部APIからデータを取得しているとします。

import { httpRequest } from "./lib/http-requesrt";

export const fetchHttpbin = async () => {
  const jsonResponse = await httpRequest({ url: "https://httpbin.org/get" });
  console.log("jsonResponse", jsonResponse);
  return jsonResponse;
};
lib/http-request.ts
import fetch from "node-fetch";

export const httpRequest = async ({ url }: { url: string }) => {
  const response = await fetch(url);
  const json = await response.json();
  return json;
};

jest.spyOn()を使う場合

import fetch, { Response } from "node-fetch";
import { fetchHttpbin } from "../app/example-serivce";

describe("データ取得", () => {
  test("httpbinのデータが取得される", async () => {
    const resp = { data: "test2" };
    jest
      .spyOn(fetch, "default")
      .mockResolvedValue(new Response(JSON.stringify(resp)));

    const result = await fetchHttpbin();

    expect(result).toEqual({
      data: "test2",
    });
  });
});

jest.mock()を使う場合

https://jestjs.io/ja/docs/mock-functions#モジュールのモック
https://jestjs.io/ja/docs/next/bypassing-module-mocks
を参考に、

jest.mock("node-fetch");
import fetch from "node-fetch";
import { fetchHttpbin } from "../app/example-serivce";
const { Response } = jest.requireActual("node-fetch");
const fetchMock = fetch as jest.MockedFunction<typeof fetch>;

describe("データ取得", () => {
  test("httpbinのデータが取得される", async () => {
    const resp = { data: "test" };
    fetchMock.mockResolvedValue(new Response(JSON.stringify(resp)));
    
    const result = await fetchHttpbin();

    expect(result).toEqual({
      data: "test",
    });
  });
});

httpRequest()

const json = await response.json();

の箇所でjsonへの変換処理を通すためにテストコード側で

const { Response } = jest.requireActual("node-fetch");

実際のnode-fetchのResponseクラスを用いています。

jest.mock("node-fetch");

node-fetchすべてがmockになりますが、Responseだけはnode-fetchの実際のものを使いたい、というときの手法になります。

おわりに

サンプルプロジェクトはこちらになります。
https://github.com/hid3h/nextjs-jest-examples/tree/main/mock-stub

この記事が少しでも参考になれば幸いです。
いいねを押してもらえれば励みになります!

Discussion