vitest-environment-miniflare から service_bindings を使用する
TL;DR
service bindings の実体である ServiceWorkerGlobalScope の fetch は、global.fetch とは振る舞いが異なります。
undici/mockAgent によるモックが効果を発揮しないので、getMiniflareBindings から得られる bindings を上書きします。
今回のサンプルリポジトリ
getMiniflareBindings から得られる bindings を上書きするところ
import app, { Bindings } from "."; // ここを 自分自身だったり外部の worker を指定する
// describe の中
let bindings: Bindings;
let mockAgent: ReturnType<typeof getMiniflareFetchMock>;
let ctx: ExecutionContext;
beforeEach(() => {
bindings = getMiniflareBindings<Bindings>();
ctx = new ExecutionContext();
bindings.SELF_SERVICE = {
fetch: (...args: ConstructorParameters<typeof Request>) => {
return app.fetch(new Request(...args), bindings, ctx);
},
} as Fetcher; // fetch しか使ってないという前提
mockAgent = getMiniflareFetchMock();
mockAgent.disableNetConnect();
});
wrangler.toml
name = "service-bindings-testing"
main = "src/index.ts"
compatibility_date = "2023-01-01"
services = [
{ binding = "SELF_SERVICE", service = "service-bindings-testing", environment = "production" }
]
はじめに
cloudflare workers を開発する際、service bindings を介した fetch の振る舞いは global.fetch と異なるのでテストが失敗するときがあります。特に、外部リクエストを伴うテストを行う際、mockAgent を使用しても、モックが正しく動作しない場合があります。
テストを行うために、getMiniflareBindings を使用して取得した bindings を上書きし、mockAgent でモックが機能するようにします。
今回は自分自身にリクエストを送る service binding と別の worker にリクエストを送る service bindings を作って両方動作することを確かめていきます。
service bindings について
worker から worker へのリクエストを行う際に使用します。また、自分自身へのリクエストを行う場合にも必要です。
Hono を vitest-environment-miniflare でテストする環境を作る
tsconfig.json で miniflare の型を読むようにする。
- "types": ["cloudflare/workers-types"],
+ "types": ["vitest/globals", "cloudflare/workers-types", "vitest-environment-miniflare/globals"],
Hono でアプリケーションを作る
それぞれ
- /
- 自分自身にリクエストを送るハンドラ
- /external
- 自分自身にリクエストし、その先で外部リクエストを行っているハンドラ
- /gateway
- 外部の worker にリクエストを送るハンドラ
import { Hono } from "hono";
export type Bindings = {
SELF_SERVICE: Fetcher;
GATEWAY: Fetcher;
};
export type Variables = {
//
};
export type HonoEnv = {
Bindings: Bindings;
Variables: Variables;
};
const app = new Hono<HonoEnv>();
app.get("/receive", (c) => {
return c.text("Hello Hono!");
});
app.get("/", (c) => {
const url = new URL("/receive", c.req.url);
const req = new Request(url.href);
return c.env.SELF_SERVICE.fetch(req);
});
app.get("/external/receive", (c) => {
return fetch("https://example.com");
});
app.get("/external", (c) => {
return c.env.SELF_SERVICE.fetch(new URL("/external/receive", c.req.url).href);
});
app.get("/gateway", (c) => {
return c.env.GATEWAY.fetch(new URL("/", c.req.url).href);
});
export default app;
wrangler.toml
wrangler.toml
name = "service-bindings-testing"
main = "src/index.ts"
compatibility_date = "2023-01-01"
services = [
{ binding = "SELF_SERVICE", service = "service-bindings-testing", environment = "production" },
{ binding = "GATEWAY", service = "gateway", environment = "production" }
]
[miniflare.mounts]
gateway = "./gateway/src"
gateway
gateway/src/index.ts
import { Hono } from "hono";
const app = new Hono();
app.get("/", (c) => {
return c.text("Hello External Hono!");
});
export default app;
wrangler.toml
name = "gateway"
main = "src/index.ts"
compatibility_date = "2023-01-01"
/
/external
/gateway
に対して getMiniflareFetchMock から得られる undici/mockAgent を使用して fetch をモックします。
/
/external
に対してテストを書く
getMiniflareBindings() を使用して取得した bindings の中の service binding を上書きし、fetch の中で app.fetch を呼び出します。この手順により、ServiceWorkerGlobalScope を経由せず、fetch を使用する部分が global.fetch だけになります。その結果、mockAgent を使用して全ての外部リクエストをインターセプトできるようになり、外部リクエストを伴うテストが可能になります。
bindings.SELF_SERVICE = {
fetch: (...args: ConstructorParameters<typeof Request>) => {
return app.fetch(new Request(...args), bindings, ctx);
},
} as Fetcher;
テストコード
import app, { Bindings } from ".";
describe("self service bindings", () => {
let bindings: Bindings;
let mockAgent: ReturnType<typeof getMiniflareFetchMock>;
let ctx: ExecutionContext;
beforeEach(() => {
bindings = getMiniflareBindings<Bindings>();
ctx = new ExecutionContext();
bindings.SELF_SERVICE = {
fetch: (...args: ConstructorParameters<typeof Request>) => {
return app.fetch(new Request(...args), bindings, ctx);
},
} as Fetcher;
mockAgent = getMiniflareFetchMock();
mockAgent.disableNetConnect();
});
it("/", async () => {
const req = new Request("http://localhost");
const res = await app.fetch(req, bindings);
expect(res.status).toBe(200);
expect(await res.text()).toBe("Hello Hono!");
});
it("/external", async () => {
const example = mockAgent.get("https://example.com");
example.intercept({ method: "GET", path: "/" }).reply(200, "Hello External Hono!");
const req = new Request("http://localhost/external");
const res = await app.fetch(req, bindings);
expect(res.status).toBe(200);
await expect(res.text()).resolves.toBe("Hello External Hono!");
});
});
/gateway
に対してテストを書く
この部分も同様です。ただし、wrangler.toml の設定が異なります。
import app, { Bindings } from ".";
import gateway from "../gateway/src";
describe("gateway service bindings", () => {
let bindings: Bindings;
let mockAgent: ReturnType<typeof getMiniflareFetchMock>;
let ctx: ExecutionContext;
beforeEach(() => {
bindings = getMiniflareBindings<Bindings>();
ctx = new ExecutionContext();
bindings.GATEWAY = {
fetch: (...args: ConstructorParameters<typeof Request>) => {
return gateway.fetch(new Request(...args), bindings, ctx);
},
} as Fetcher; // fetch しか使ってないという前提
mockAgent = getMiniflareFetchMock();
mockAgent.disableNetConnect();
});
it("/gateway", async () => {
const req = new Request("http://localhost/gateway");
const res = await app.fetch(req, bindings);
expect(res.status).toBe(200);
await expect(res.text()).resolves.toBe("Hello External Hono!");
});
});
wrangler.toml に以下を増やす。
+ [miniflare.mounts]
+ gateway = "./gateway/src" // ここは gateway の hono app があるファイルを指す
確認する
以上の変更を加えた状態でテストができていることを見てみます。
npm run test
通りました。
今回のサンプルリポジトリです。
ありがとうございました。
Discussion