はじめに
Cloudflare Workers + D1 を使ったアプリケーションを作っています。
D1 に対して読み書きする処理のテストを書きたかったのですが、D1 は Workers のランタイム上で動く SQLite ベースの DB なので、素朴に Node.js 上の Vitest でモックしてテストするのは実装との乖離が大きくなりそうでした。
調べてみると @cloudflare/vitest-pool-workers という、Vitest のテストを workerd の中で実行できる仕組みが公式から提供されていて、D1 や R2 などの binding をそのまま使ってテストが書けることが分かったので、これを使って試してみました。
vitest-pool-workers とは
@cloudflare/vitest-pool-workers を使うと、Vitest のテストコードを Workers のランタイム上で実行できます。
wrangler.jsonc(または wrangler.toml)の設定を読み込んで、D1・R2・KV といった binding を シミュレータ(Miniflare)経由でテスト内からそのまま使えるようにしてくれます。
テスト実行時に migration を適用する
D1 に対する処理をテストするには、テスト実行前に migration を適用しておく必要があります。
設定ファイルでは readD1Migrations で migration ファイルを読み込み、miniflare.bindings 経由でテスト側に渡すようにします。
※ この例では、migration ファイルが、src/server/db/migrations 配下に置かれている想定です。
import path from "node:path";
import {
cloudflareTest,
readD1Migrations,
} from "@cloudflare/vitest-pool-workers";
import { defineConfig } from "vitest/config";
export default defineConfig(async () => {
const migrationsPath = path.join(
import.meta.dirname,
"src/server/db/migrations",
);
const migrations = await readD1Migrations(migrationsPath);
return {
plugins: [
cloudflareTest({
wrangler: { configPath: "./wrangler.jsonc" },
miniflare: {
bindings: {
TEST_MIGRATIONS: migrations,
},
},
}),
],
test: {
setupFiles: ["./test/apply-migrations.ts"],
},
};
});
実際に migration を適用する処理は setupFiles に渡した test/apply-migrations.ts に書きます。
import { applyD1Migrations } from "cloudflare:test";
import { env } from "cloudflare:workers";
await applyD1Migrations(env.DB, env.TEST_MIGRATIONS);
※ env.DB の DB は、wrangler.jsonc の d1_databases に設定した binding の名前です。
実際にテストを書く
私は D1 へのアクセスには drizzle-orm/d1 を使っていて、Web フレームワークとしては Hono を使っています。
テスト対象のアプリケーションと同様に、env.DB をそのまま drizzle に渡してしまえばいいので、以下のように書くことができます。
import { env } from "cloudflare:workers";
import { drizzle } from "drizzle-orm/d1";
import { describe, expect, it } from "vitest";
import { items } from "./db/schema";
import app from "./index";
describe("GET /", () => {
it("returns 200 and renders items from D1", async () => {
const db = drizzle(env.DB);
await db.insert(items).values({
name: "Test Item",
quantity: 3,
price: 1500,
description: "Test description",
imageUrl: "/images/test.jpg",
});
const res = await app.request("/", {}, env);
expect(res.status).toBe(200);
const html = await res.text();
expect(html).toContain("Test Item");
});
});