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

Posts

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