Cloudflare D1のテストをbun:sqliteを使って書く
米国株が200円から買えるwoodstock.clubというアプリを開発する会社で働いているitomeです。
TL;DR
- Cloudflare D1はSQLiteベースのDBなので、テストのときだけbun:sqliteを使うことができる
- 組み込みのDBを使うからwranglerを使うより簡単で速い(サンプルリポジトリだとGitHub Actions上で10秒~でテストが通る)
- bun:sqliteはインメモリDBとしても使えるので、テストケースごとにDBを作成できてテストを並列化したり実行順を入れ替えてもフレーキーになりづらい
- マイグレーションやbun:sqliteに対応していないbatchなどは別途対応が必要
セットアップ
今回はHonoとDrizzle ORMを使って簡単なAPIサーバーを作り、そのテストをbun:sqliteを使って書いてみます。
$ bun create hono@latest cloudflare-d1-bun-sqlite-sample
$ bun add drizzle-orm
$ bun add -D drizzle-kit
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
email: text("email").notNull(),
});
$ drizzle-kit generate
$ wrangler d1 migrations apply my-database --local
DBにアクセスするクラスを作成
HonoのAPIごとテストを書いてもいいですが、DBにアクセスする部分のみを単体でテストするため、リポジトリクラスを作ります。
import { users } from "../schema";
import { DB } from "./db";
export class UserRepository {
private db: DB;
constructor(db: DB) {
this.db = db;
}
async getAll(): Promise<(typeof users.$inferSelect)[]> {
return this.db.select().from(users).all();
}
}
このクラスで使うDB
型は、bunとcloudflare d1を抽象化するため、drizzleのBaseSQLiteDatabase
をラップしたものです。
export type DB = BaseSQLiteDatabase<any, any, typeof schema>;
Cloudflare D1を使う場合は↓
import { drizzle as d1Drizzle } from "drizzle-orm/d1";
import * as schema from "../schema";
export const createDB = (db: D1Database): DB => {
return d1Drizzle(db, { schema });
};
bun:sqliteを使う場合は↓
import { Database } from "bun:sqlite";
import { drizzle as bunDrizzle } from "drizzle-orm/bun-sqlite";
import * as schema from "../schema";
export const createTestDB = (): DB => {
return bunDrizzle(new Database(":memory:"), { schema });
};
のように初期化します。bun:sqliteは初期化時にDBファイルへのパスの代わりに":memory:"
という文字列を渡すとインメモリDBとして使うことができます。
Honoで使う
実際のコードで使うときは、HonoのBindingsからD1のインスタンスを取得してリポジトリを作成します。
import { Hono } from "hono";
import { createDB } from "./repository/db";
import { UserRepository } from "./repository/user";
type Bindings = {
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>();
app.get("/users", async (c) => {
const db = createDB(c.env.DB);
const repository = new UserRepository(db);
const result = await repository.getAll();
return c.json(result);
});
export default app;
テストで使う
テストコードで使うときは、テストごとにbun:sqliteのDBを作成します。またbeforeAll
でマイグレーションが必要です。
import {
afterEach,
beforeAll,
beforeEach,
describe,
expect,
test,
} from "bun:test";
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
import { createTestDB } from "./db.test";
import { UserRepository } from "./user";
import { users } from "../schema";
describe("UserRepository", () => {
const db = createTestDB();
const repository = new UserRepository(db);
beforeAll(async () => {
migrate(db, { migrationsFolder: "migrations" });
});
beforeEach(async () => {
await db.insert(users).values([
{
name: "John Doe",
email: "foo@bar.com",
},
{
name: "Taro Yamada",
email: "hoge@fuga.com",
},
]);
});
afterEach(async () => {
await db.delete(users);
});
test("get all users", async () => {
const users = await repository.getAll();
expect(users).toHaveLength(2);
});
});
シンプルなテストであればこれくらいの時間でテストが完了します。これならbun test --watch
コマンドでテストを走らせっぱなしにしても十分実用に耐えると思います。
デメリット
Cloudflare D1には複数のクエリを一度に実行するbatch
機能があり、drizzleもそれをサポートしています。
しかしbun:sqliteではbatchをサポートしていないため、テストを書くことができません。
batch
を使うコードをテストしたい場合は、その部分だけMiniflareを使ってテストを書くことで回避できますが、もっとスマートな解決策を知っている方がいたら共有してもらえるとありがたいです。
サンプルコード
Discussion