Hono on Cloudflare Workersのテストを書くときにハマったこと

Blog

Hono on Cloudflare Workersを使って実装したWebアプリケーションのテストを書いたとき、いくつかハマったことがあったのでその解決策について記載する。

Honoはjest-environment-miniflareを使ったテストを推奨しているため、Miniflareによる仮想環境と、テストフレームワークであるJestを利用した。

  • Miniflare:Cloudflare Workersを開発/テストするためのシミュレーター
  • Jest:テストフレームワーク

確認した環境

  • wrangler v2.15.1
  • Hono v3.1.5
  • jest v29.5.0

HTTPレスポンスのモックにはnockが使えない

発生したこと

外部へのリクエスト結果をモックするため、次のようにnockを利用しようとした。
しかし、モックされずに、https://example.comへリクエストが送られてしまった。

src/index.test.ts
// 以下は example.com へのリクエストレスポンスが mock されない

describe("GET /get", () => {
  beforeAll(() => {
    nock("https://example.com").get('/404').reply(404);
  });

  it("should be 404", async () => {
    const res = await app.request(`https://localhost/get?url=https://example.com/404`);
    expect(res.status).toBe(404);
  });
});

原因

nockはhttphttpsおよびXMLHttpRequestのモジュールを使ったfetchリクエストをモックするライブラリである。
一方で、Miniflare v2以降のfetchの実装には、undiciというHTTPクライアントを利用している。
そのため、HTTPリクエストレスポンスがモックされなかった。

Hey @mrbbot, thanks for all the work on miniflare, it's been a game-changer! With miniflare 1, I've been using mock service worker to mock API responses. When attempting to upgrade to miniflare 2.2...
GitHub

解決策

HTTPレスポンスをモックするには、jest-environment-miniflaregetMiniflareFetchMock()を使う。

src/index.test.ts
// getMiniflareFetchMock() を使っているため example.com へのレスポンスが mock される

describe("GET /ics", () => {
  beforeAll(() => {
    const fetchMock = getMiniflareFetchMock();
    fetchMock.disableNetConnect();
    const origin = fetchMock.get("https://example.com");
    origin.intercept({ method: "GET", path: "/404" }).reply(404, "not found");
  });

  it("should be 404", async () => {
    const res = await app.request(`https://localhost/get?url=https://example.com/404`);
    expect(res.status).toBe(404);
  });
});

getMiniflareFetchMock()を型解決するためには、tsconfig.jsonに以下を追記する。

tsconfig.json
    "types": [
      "@cloudflare/workers-types",
      "@types/jest",
+      "jest-environment-miniflare/globals"
    ],

__STATIC_CONTENT_MANIFESTモジュールがないというエラーが発生する

Honoでは、ソースコードディレクトリ以外に置いた静的ファイルを配信するためのアダプターがある。
アダプターを利用するには、GETメソッドのURLパスに、serveStatic()を渡す。

import { Hono } from "hono";
import { serveStatic } from "hono/cloudflare-workers";

// 略

const app = new Hono();
app.get("/static/*", serveStatic({ root: "./" }));

静的ファイルを置いたディレクトリは、wrangler.tomlで指定する。

wrangler.toml
name = ""
compatibility_date = "2023-01-01"

+ [site]
+ bucket = "./public"

このように記載すると、public/static以下に置いたファイルは/static/以下のパスで配信される。
たとえば、次のようにおいたpublic/static/foo.png/static/foo.pngで配信される。

.
├── README.md
├── //
├── public
   └── static
       └── foo.png
├── src
   ├── index.test.tsx
   └── index.tsx
├── tsconfig.json
└── wrangler.toml

発生したこと

テストを実行すると、__STATIC_CONTENT_MANIFESTのモジュール解決に失敗したというエラーが発生した。

$ npx jest

 FAIL  src/index.test.tsx
 Test suite failed to run

    Cannot find module '__STATIC_CONTENT_MANIFEST' from 'node_modules/hono/dist/cjs/adapter/cloudflare-workers/server-static-module.js'

    Require stack:
      node_modules/hono/dist/cjs/adapter/cloudflare-workers/server-static-module.js
      node_modules/hono/dist/cjs/adapter/cloudflare-workers/index.js
      src/index.tsx
      src/index.test.tsx

原因

miniflareでは、静的ファイルの情報はWorkers KVへ格納されるため、その型情報は__STATIC_CONTENT_MANIFESTに保存される。
しかし、jest-environment-miniflareで動かすときには__STATIC_CONTENT_MANIFESTが存在せず、モジュールを解決できない。

解決策

静的ファイルの情報を作成するため、__STATIC_CONTENT_MANIFESTのスタブを作成する。

src/manifest.ts
/**
 * __STATIC_CONTENT_MANIFEST stub
 */
export default "{}";
jest.config.js
module.exports = {
  testMatch: ["**/*.test.(ts|tsx)"],
  // 略
  testEnvironment: "miniflare",
+  moduleNameMapper: {
+    __STATIC_CONTENT_MANIFEST: "<rootDir>/src/manifest.ts",
+  },
};

参考にしたページ

feat: Add __STATIC_CONTENT_MANIFEST to Jest config #22