Cloudflare Workers + D1 + Hono + VitestでAPIのテストを書く
概要
Cloudflare Workers + D1 + Honoで作成したAPIをVitestでローカルのテストを実行する。
環境
- pnpm
- node: 20.9.0 ※ 18系だとテストが上手く動かなかった
- hono: 4.1.4
- wrangler: 3.32.0
- vitest: 1.4.0
Honoのプロジェクトをセットアップする
基本以下ページを参考に実施。
pnpm create hono my-app
実行時にテンプレートを選択できるため、cloudflare-workers
を選択。
※ パッケージ管理はpnpm
create-hono version 0.5.0
✔ Using target directory … my-app
? Which template do you want to use? › - Use arrow-keys. Return to submit.
aws-lambda
bun
cloudflare-pages
❯ cloudflare-workers
deno
fastly
lambda-edge
netlify
nextjs
↓ nodejs
選択が完了すると、 cloudflare-workers
のパッケージをインストール済みの Honoプロジェクトが作成される。
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Hono!')
})
export default app
{
"scripts": {
"dev": "wrangler dev src/index.ts",
"deploy": "wrangler deploy --minify src/index.ts"
},
"dependencies": {
"hono": "^4.1.4"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240208.0",
"wrangler": "^3.32.0"
}
}
cloudflareのsdk cloudflare
でdev/deployがセットアップされている。
D1 Databaseをセットアップする
公式のチュートリアルは以下。基本はこちらをベースに進める。
Databaseの作成
wranglerコマンドで d1 データベースを作成する。
pnpx wrangler d1 create test-db
成功すると、指定したデータベース名でdatabase_idが発行されるため、表示されているtomlを、前項で作成したHonoプロジェクト配下にある wrangler.toml
に追記する。
✅ Successfully created DB 'test-db' in region APAC
Created your database using D1's new storage backend. The new storage backend is not yet
recommended for production workloads, but backs up your data via point-in-time restore.
[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "test-db"
database_id = "<some database id>"
name = "my-app"
compatibility_date = "2023-12-01"
[[d1_databases]]
binding = "DB"
database_name = "test-db"
database_id = "<some database id>"
テーブル定義とテストデータの反映をローカルで実行する
以下、テーブル定義とテストデータのSQLを用意。
-- テーブル定義
DROP TABLE IF EXISTS Customers;
CREATE TABLE IF NOT EXISTS Customers (CustomerId INTEGER PRIMARY KEY, CompanyName TEXT, ContactName TEXT);
-- テストデータ
INSERT INTO Customers
(CustomerID, CompanyName, ContactName)
VALUES
(1, 'Alfreds Futterkiste', 'Maria Anders'),
(4, 'Around the Horn', 'Thomas Hardy'),
(11, 'Bs Beverages', 'Victoria Ashworth'),
(13, 'Bs Beverages', 'Random Name');
wranglerコマンドを実行。
npx wrangler d1 execute test-db --local --file=./schema.sql
--local
オプションを付けることで、ローカル環境にのみ反映される。
> ls -la .wrangler/state/v3/d1/miniflare-D1DatabaseObject/
drwxr-xr-x mor staff 160 B Thu Mar 28 07:10:39 2024 .
drwxr-xr-x mor staff 128 B Thu Mar 28 07:11:00 2024 ..
.rw-r--r-- mor staff 4.0 KB Thu Mar 28 07:10:39 2024 64a822b5d4d8692f79c45668bfae1c27a735245aedd4305363c46a441c792858.sqlite
.rw-r--r-- mor staff 32 KB Thu Mar 28 07:10:39 2024 64a822b5d4d8692f79c45668bfae1c27a735245aedd4305363c46a441c792858.sqlite-shm
.rw-r--r-- mor staff 16 KB Thu Mar 28 07:10:39 2024 64a822b5d4d8692f79c45668bfae1c27a735245aedd4305363c46a441c792858.sqlite-wal
.wrangler配下にsqliteが作られる。
HonoからD1データベースにアクセスする
公式が資料を用意してくれているのでこちらを参考に進める。
Customersテーブルのレコードを返すエンドポイントを追加
honoのルーティングに GET /customers
というエンドポイントを作成し、前述で作成したCustomersテーブルのレコードを全て返すように実装する。
import { Hono } from "hono";
type Bindings = {
DB: D1Database;
};
# d1データベースの型をHonoのEnvに定義する
const app = new Hono<{ Bindings: Bindings }>();
app.get("/customers", async (c) => {
try {
# c(context).envからD1のデータベースにアクセスできる
let { results } = await c.env.DB.prepare("SELECT * FROM customers").all();
return c.json(results);
} catch (e) {
return c.json({ err: e }, 500);
}
});
export default app;
devサーバを起動し、データが取得できているか確認。
pnpm dev
> curl http://localhost:8787/customers
[{"CustomerId":1,"CompanyName":"Alfreds Futterkiste","ContactName":"Maria Anders"},{"CustomerId":4,"CompanyName":"Around the Horn","ContactName":"Thomas Hardy"},{"CustomerId":11,"CompanyName":"Bs Beverages","ContactName":"Victoria Ashworth"},{"CustomerId":13,"CompanyName":"Bs Beverages","ContactName":"Random Name"}]%
vitestのセットアップ
以下の公式ページを参考。
pnpm add -D vitest
設定
describeやitをimportしたくなかったため、以下のglobalの設定のみ追加した。
/// <reference types="vitest" />
import { defineConfig } from "vitest/config";
// https://vitejs.dev/config/
export default defineConfig({
test: {
globals: true,
},
});
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"lib": ["ESNext"],
"types": [
"@cloudflare/workers-types",
+ "vitest/globals"
],
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}
テストコマンドをpackage.jsonに追加。
{
"scripts": {
"dev": "wrangler dev src/index.ts",
"deploy": "wrangler deploy --minify src/index.ts",
+ "test": "vitest run"
},
"dependencies": {
"hono": "^4.1.4"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240208.0",
"vitest": "^1.4.0",
"wrangler": "^3.32.0"
}
}
APIのテストを書く
d1のローカル環境を作成する
d1とテスト環境で動作させるために、unstable_dev()
というAPIを使用して、テスト用のワーカーを立ち上げる。
詳細は以下ページも参照。
まずは、index.tsのテストファイルを作成。
import { unstable_dev } from "wrangler";
import type { UnstableDevWorker } from "wrangler";
describe("GET /customers", async () => {
let worker: UnstableDevWorker;
// UnstableDevWorkerは起動と停止に時間がかかるためbeforeAllで実行する
beforeAll(async () => {
// 読み込むtsファイルを指定
worker = await unstable_dev("./src/index.ts", {
experimental: {
disableExperimentalWarning: true,
},
});
});
// afterAllで停止
afterAll(async () => {
await worker.stop();
});
});
/customersのテストを書く
import { getPlatformProxy, unstable_dev } from "wrangler";
import type { UnstableDevWorker } from "wrangler";
// index.tsからappをimportする
import app from "./index";
// getPlatformProxyを使ってd1のenvを取得する
const { env } = await getPlatformProxy();
describe("GET /customers", async () => {
let worker: UnstableDevWorker;
// UnstableDevWorkerは起動と停止に時間がかかるためbeforeAllで実行する
beforeAll(async () => {
// 読み込むtsファイルを指定
worker = await unstable_dev("./src/index.ts", {
experimental: {
disableExperimentalWarning: true,
},
});
});
// afterAllで停止
afterAll(async () => {
await worker.stop();
});
it("正しくレスポンスが取得できること", async () => {
const res = await app.request(
"/customers",
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
// d1のenvをappの第3引数に渡すと、app内のenv.DBにd1のenvがバインドされる
env
);
expect(res.status).toBe(200);
const responseBody = await res.json();
expect(responseBody).toEqual([
{
CompanyName: "Alfreds Futterkiste",
ContactName: "Maria Anders",
CustomerId: 1,
},
{
CompanyName: "Around the Horn",
ContactName: "Thomas Hardy",
CustomerId: 4,
},
{
CompanyName: "Bs Beverages",
ContactName: "Victoria Ashworth",
CustomerId: 11,
},
{
CompanyName: "Bs Beverages",
ContactName: "Random Name",
CustomerId: 13,
},
]);
});
});
HonoでAPIのテストを書くときは、index.tsからappをimportして app.request
で対称のエンドポイントを実行すればOK。
import app from "./index";
ただし、そのまま実行するとd1データベースがバインドされていないため、requestの引数からenvを渡す。
// getPlatformProxyを使ってd1のenvを取得する
const { env } = await getPlatformProxy();
・・・
const res = await app.request(
"/customers",
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
// d1のenvをappの第3引数に渡すと、app内のenv.DBにd1のenvがバインドされる
env
);
getPlatformProxy
を使うことで、workerbにバインドされているobjectを取得することができる。
> pnpm test
> @ test /Users/mor/repos/my-work/github.com/morooka-akira/my-app
> vitest run
The CJS build of Vite's Node API is deprecated. See https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.
RUN v1.4.0 /Users/mor/repos/my-work/github.com/morooka-akira/my-app
✓ src/index.test.ts (1) 1122ms
✓ GET /customers (1) 1122ms
✓ 正しくレスポンスが取得できること
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 08:20:15
Duration 2.75s (transform 1.00s, setup 0ms, collect 1.49s, tests 1.12s, environment 0ms, prepare 36ms)
Discussion