🌧️

Cloudflare D1のテストをbun:sqliteを使って書く

2024/08/29に公開

米国株が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を使って書いてみます。

honoプロジェクトを作成
$ bun create hono@latest cloudflare-d1-bun-sqlite-sample
drizzleをインストール
$ bun add drizzle-orm
$ bun add -D drizzle-kit
schemaを定義
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を使ってテストを書くことで回避できますが、もっとスマートな解決策を知っている方がいたら共有してもらえるとありがたいです。

サンプルコード

https://github.com/itome/cloudflare-d1-bun-sqlite-sample

Discussion