JavaScriptではSinonをはじめとして、さまざまなモックの仕組みがあります。
その中で、最近Mock Service Worker(以下msw)が良いよ、とNode.js会長に教えてもらいました。
mswはブラウザではService Workerとして動き、通信をフックすることでフロントのJavaScriptコードからは本物のサーバーに見えるのが特徴です。また、Node.jsでも動きます。isomorphicではないのですが、モック用のハンドラー、ブラウザ用、Node.js用、エントリーポイントとファイルを4分割して作成すると、ブラウザ単体でもSPAとして動きつつ、サーバーサイドレンダリングでNode.jsでも動作するというNext.jsでも動作します。
Sinonでテストを書くと、どうしても内部実装べったりな壊れやすいテストになるのですが、mswだと疑似的なサーバーを実装している感じで書けますので、素結合な感じになります。
便利なのでJestのユニットテストでも使ってみようと思ったところ少し情報が少なかったり見つけにくかったりまとまってなかったのでいくつか紹介します。
setupServerだけでいい
よくある利用例だとsetupWorkerを使ったり、環境によってsetupServerとsetupWorkerを呼び分けますが、JestのテストだとsetupServerだけでよいです。テストコードの中にインラインで書けるので影響範囲が分かりやすくて読みやすいテストになりますよね。
describe("通信をモックするテスト", () => {
const server = setupServer(
rest.get("/hello", (req, res, ctx) => {
return res(ctx.json({ message: "hello world" }));
})
);
beforeEach(() => {
server.listen();
});
afterEach(() => {
server.close();
});
// ここにテストをかける
});
fetchのPolyfillが必要
他のサンプルはブラウザ前提なのか書いてないのが多いのですが、Node.js環境だとfetchなんて定義されてない!というエラーが出ます。Polyfillを足す必要があります。
$ npm i -D whatwg-fetch
テストが書かれているスクリプトにそれぞれ以下のimport文を足します。
import "whatwg-fetch"
マッチしているかどうかの確認
モックを利用していて一番もやもやするのが、本当に正しく呼ばれているの?という点です。ウェブアプリケーションフレームワークだとnotFoundハンドラーとかあったりしますが、mswには残念ながらありません。で上から評価されるのかというとそういうわけでもなさそうで、範囲の広いものを書くとそっちにマッチしちゃいます。
とりあえず既成の通信がよくわからない部品をテストする場合、こういう書き方をすると、全部ダミーの方に吸われちゃいます。
const server = setupServer(
rest.get("/hello", (req, res, ctx) => {
// なぜかこっちが呼ばれなくなる
return res(ctx.json({ message: "hello world" }));
}),
rest.get("/*", (req, res, ctx) => {
// 試しに呼ばれたURLをダンプしてみる
console.log(`🐣 ${req.url.toString()}`);
return res(ctx.json({ dummy: "call test" }));
}),
);
最初は全部扱うアスタリスクのハンドラを書いておいて、その中でswitch文で分岐させて、全部の通信が明らかになったらいつもの書き方に・・・というのがいいのかな、と考えているところです。
クエリーのマッチ
URLの末尾のクエリーにマッチするのはrest.get()の引数ではなくて、本体の中で取得します。
rest.get("/hello", (req, res, ctx) => {
const name = req.url.searchParams.get("name");
return res(ctx.json({ hello: name }));
}),
モック通信待ち
useSWRを使ってサーバーから取ってきた内容を表示するコンポーネントを作ったとします。そのまま走らせると、ダウンロードが完了する前にテストが走り切ってしまうのでテストに失敗してしまいます。
import useSWR from "swr";
function Message() {
const { data, error } = useSWR("/hello", fetcher);
if (!data && !error) {
return <div>loading</div>;
} else if (error) {
return <div>{error}</div>;
}
const { message } = data;
return <div data-testid="loaded">{message}</div>;
}
@testing-library/reactであれば、waitForというのがいるのでこれを使います。ロード中か完了したのかが即座にわかるような書き方をしていれば(ここでは完了後にdata-testidがloadedになる前提)、こんな感じで書けます。
import { render, screen, waitFor } from "@testing-library/react";
// ここにモックサーバーを置く
test("Message", async () => {
render(<Sample />);
await waitFor(() => screen.getByTestId("loaded"));
expect(screen.getByTestId("loaded").innerHTML).toBe("hello world");
});
まとめ
調べてみたけど試行錯誤したりとかいろいろなサイトを見たらようやく出てきたものをまとめました。まあ情報が少ないというだけで使い勝手はかなり良いので、今後は積極的に使っていきたいと思います。Reactのカスタムフックのテストとかはかどりますよ。