🌩️

vitest-environment-miniflare から service_bindings を使用する

2023/10/25に公開

TL;DR

service bindings の実体である ServiceWorkerGlobalScope の fetch は、global.fetch とは振る舞いが異なります。
undici/mockAgent によるモックが効果を発揮しないので、getMiniflareBindings から得られる bindings を上書きします。

https://github.com/cloudflare/miniflare/blob/f919a2eaccf30d63f435154969e4233aa3b9531c/packages/core/src/standards/event.ts#L371C16-L441

今回のサンプルリポジトリ
https://github.com/naporin0624/vitest-miniflare-environment-mock

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 でモックが機能するようにします。

mock が失敗してエラーをはいているさま

今回は自分自身にリクエストを送る service binding と別の worker にリクエストを送る service bindings を作って両方動作することを確かめていきます。

service bindings について

worker から worker へのリクエストを行う際に使用します。また、自分自身へのリクエストを行う場合にも必要です。

https://developers.cloudflare.com/workers/configuration/bindings/about-service-bindings/

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 をモックします。
https://github.com/cloudflare/miniflare/blob/master/docs/src/content/testing/jest.md#mocking-outbound-fetch-requests

https://github.com/nodejs/undici/blob/main/docs/api/MockAgent.md

/ /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

通りました。

vitest で service bindings を使用した workers のテストができているさま

今回のサンプルリポジトリです。
https://github.com/naporin0624/vitest-miniflare-environment-mock

ありがとうございました。

Discussion