📖
Jestでmock, stubする
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したいので以下に倣います。
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
の場合はファイルのトップレベルで呼び出す必要があります。
を参考に以下のようになります。
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()を使う場合
を参考に、
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
の実際のものを使いたい、というときの手法になります。
おわりに
サンプルプロジェクトはこちらになります。
この記事が少しでも参考になれば幸いです。
いいねを押してもらえれば励みになります!
Discussion