🙆

Cloudflare Workers + D1 + Hono + VitestでAPIのテストを書く

2024/03/28に公開

概要

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のプロジェクトをセットアップする

基本以下ページを参考に実施。
https://hono.dev/getting-started/cloudflare-workers

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プロジェクトが作成される。

index.ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  return c.text('Hello Hono!')
})

export default app
package.json
{
  "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をセットアップする

公式のチュートリアルは以下。基本はこちらをベースに進める。

https://developers.cloudflare.com/d1/get-started/

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>"
wrangler.toml
name = "my-app"
compatibility_date = "2023-12-01"

[[d1_databases]]
binding = "DB"
database_name = "test-db"
database_id = "<some database id>"

テーブル定義とテストデータの反映をローカルで実行する

以下、テーブル定義とテストデータのSQLを用意。

schema.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データベースにアクセスする

公式が資料を用意してくれているのでこちらを参考に進める。

https://developers.cloudflare.com/d1/examples/d1-and-hono/

Customersテーブルのレコードを返すエンドポイントを追加

honoのルーティングに GET /customers というエンドポイントを作成し、前述で作成したCustomersテーブルのレコードを全て返すように実装する。

index.ts
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のセットアップ

以下の公式ページを参考。

https://vitest.dev/guide/

pnpm add -D vitest

設定

describeやitをimportしたくなかったため、以下のglobalの設定のみ追加した。

vite.config.ts
/// <reference types="vitest" />

import { defineConfig } from "vitest/config";

// https://vitejs.dev/config/
export default defineConfig({
  test: {
    globals: true,
  },
});
tsconfig.json
{
  "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に追加。

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を使用して、テスト用のワーカーを立ち上げる。

詳細は以下ページも参照。

https://developers.cloudflare.com/d1/configuration/local-development/#usage-example

https://developers.cloudflare.com/workers/wrangler/api/#usage

まずは、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を取得することができる。

https://developers.cloudflare.com/workers/wrangler/api/#getplatformproxy

> 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