Cloudflare Workersでも自動テストがしたい

　アプリケーションを作成するとき、やらなきゃいけないのはCI / CD環境の構築ですよね。
 　[Cloudflareで作成したアプリ](https://qiita.com/ogadra/items/b7e0d790a8f4105cf99c)のCI環境を（リリース後ですが）整えたので、それについて述べたいと思います。

　著者自身vitestを初めて用いるため、ツッコミどころや「そもそもテストの書き方が良くないだろ」という箇所があるかもしれません。マサカリ歓迎です。

※ 記事内に出てくるソースコードの動作確認はしていません。あくまでも参考としてご活用ください。

## 要約

- `unstable_dev`でwranglerを立ち上げてテストする
- データベースに対する操作はsqliteファイルを直接操作する

## 使用技術について

　下記パッケージ・サービス等を利用しました。

 - Cloudflare Workers
     - 主役
 - Cloudflare D1
     - Cloudflareで使える、SQLiteベースのRDB
 - vitest
     - テストフレームワーク
 - Drizzle
     - ORM
 - GitHub Actions
     - CI実行環境

## テスト環境を整えてみる
### ローカル

　2024年9月現在、wranglerでのテスト方法は3通りあります。

 - [vitest-pool-workers](https://developers.cloudflare.com/workers/testing/vitest-integration/get-started/write-your-first-test/)
 - wranglerの[unstable_dev](https://developers.cloudflare.com/workers/wrangler/api/#unstable_dev)
 - [miniflare](https://github.com/cloudflare/miniflare?tab=readme-ov-file)を使う

　最新は`vitest-pool-workers`ですが、バージョンが0系であることもあり、まだまだ不安定です。私の環境では[バグを踏んで](https://github.com/cloudflare/workers-sdk/issues/6715)うまく動きませんでした。よって、`unstable_dev`を用いる方法でテストを書いています。

#### 書いていく

　基本的な流れは

 - `unstable_dev`でのwrangler立ち上げ
 - テスト実行
 - wrangler終了

ですね。

```setup.ts
import { afterAll, beforeAll } from "vitest";
import {
    type UnstableDevWorker,
    unstable_dev,
} from "wrangler";

let worker: UnstableDevWorker;

beforeAll(async () => {
	worker = await unstable_dev("./server.ts", {
		config: "wrangler.test.toml",
		experimental: {
			disableExperimentalWarning: true,
		},
	});
});

afterAll(async () => {
	await worker.stop();
});

export { worker };
```

　configとして、`wrangler.test.toml`と本番環境とは異なる設定ファイルを用意しています。これにより、使用DBをローカル環境とテスト環境で分けることが可能です。著者は、下記のようにD1を使い分けています。

 - Production環境（リモート）
    - `database_name = app-prd`
 - Staging環境（リモート）
    - `database_name = app-stg`
 - 開発環境（ローカル）
    - `database_name = app-prd`
 - テスト環境（ローカル）
    - `database_name = app-stg`

　wranglerでは、ローカル環境に複数のDBを用意するために複数のdatabase_idが必要です。そのため、リモート環境でDBを使い分けていることを利用し、ローカル環境に複数DBを実現しています。（テスト実行用のDB、分けたいですよねぇ）

　テストコードは下記のように書きます。

```index.test.ts
import { describe, expect, it } from "vitest";
import { worker } from "./setup";

describe("GET /", async () => {
	it("インデックスページを取得したとき200OK", async () => {
		const response = await worker.fetch("/");
		expect(response.status).toBe(200);
	});
});
```


#### 環境変数

　環境変数も必要ですね。`.env.test`を使うように設定しておきます。

```shell:.env.test
SOMETHING_SECRET=very-important-secret
```

 ```vitest.config.ts
import dotenv from "dotenv";
import { defineConfig } from "vitest/config";

export default defineConfig({
    test: {
        env: dotenv.config({ path: ".env.test" }).parsed,
    },
});
```

　wranglerに環境変数を渡す場合、`setup.ts`にも変更が必要です。

 

```diff_typescript:setup.ts
import { afterAll, beforeAll } from "vitest";
import {
    type UnstableDevWorker,
    unstable_dev,
} from "wrangler";

let worker: UnstableDevWorker;

beforeAll(async () => {
	worker = await unstable_dev("./server.ts", {
		config: "wrangler.test.toml",
		experimental: {
			disableExperimentalWarning: true,
		},
+		vars: {
+			SOMETHING_SECRET: process.env.SOMETHING_SECRET ?? "",
+		},
	});
});

afterAll(async () => {
	await worker.stop();
});

export { worker };
```

#### DBに初期データを投入したい

　やはりテストを実行するのにあたって、DBにデータを投入しておくのは必須ですよね。
　`wrangler`のD1部分だけを切り出してクエリ実行できるのが理想かな、と思いますができなかったため、力技ですが**wranglerで生成されるSQLiteファイルを操作する**方法で実現したいと思います。

　まずはDBを作っていきます。
```shell
$ wrangler d1 create app-stg --config=wrangler.test.toml
```

　すると、`.wrangler/state/v3/d1/miniflare-D1DatabaseObject/`ディレクトリにSQLiteファイルが生成されます。こちらを後ほど使用しますので、環境変数に設定しておきます。


```diff_shell:.env.test
SOMETHING_SECRET=very-important-secret
+ SQLITE_PATH=".wrangler/state/v3/d1/miniflare-D1DatabaseObject/xxxx.sqlite"
```

　DrizzleでSQLiteに接続するためには、別途アダプタが必要です。今回は`better-sqlite3`を使用します。テストでしか用いませんので、devDependenciesに追加しましょう。

 ```shell
 $ npm i -D better-sqlite3
 ```

`setup.ts`でDBに接続していきます。

 ```diff_TypeScript:setup.ts
+ import * as schema from "@/repository/schema";
+ import Database from "better-sqlite3";
+ import {
+	type BetterSQLite3Database,
+	drizzle,
+} from "drizzle-orm/better-sqlite3";
import { afterAll, beforeAll } from "vitest";
import {
    type UnstableDevWorker,
    unstable_dev,
} from "wrangler";


let worker: UnstableDevWorker;
+ let db: BetterSQLite3Database<Record<string, unknown>>;

beforeAll(async () => {
    worker = await unstable_dev("./server.ts", {
        config: "wrangler.test.toml",
        experimental: {
            disableExperimentalWarning: true,
        },
        vars: {
            SOMETHING_SECRET: process.env.SOMETHING_SECRET ?? "",
        },
	});

+	const sqlite = new Database(process.env.SQLITE_PATH ?? "");
+	db = drizzle(sqlite, { schema });
});

afterAll(async () => {
	await worker.stop();
});

- export { worker };
+ export { db, worker };
```

　接続できたら実際にコードを書いていきます。Drizzleを用いてDBを操作できます。


```article.test.ts
import { eq } from "drizzle-orm";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { articles } from "@/repository/schema";
import { db, worker } from "./setup";

describe("GET /articles", async () => {
	beforeAll(async () => {
		await db.insert(articles).values({
            // なにかデータを入れる
		});
	});

	afterAll(async () => {
		await db.delete(users).where(eq(articles.id, 1));
	});


	it("記事一覧に色々表示される", async () => {
        const response = await worker.fetch(`/articles/1`);
        expect(response.body).toEqual({}) // 返ってくるべき値を設定しておく
	});
});
```

　これだけではまだ動きません。`app-stg` をローカル環境に用意してマイグレーションをする必要があります。

```shell
$ npx wrangler d1 migrations apply app-stg --local --config=wrangler.test.toml
```

　を実行しておきましょう。（マイグレーションの度に再実行が必要ですね。）

　これでローカル環境において、wranglerのテスト実行ができるようになりました :tada: 

![vitest-local.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596394/4bb7360c-ba25-55e9-07a6-665647ef637c.png)
<font color="#888">
ローカル環境のCUIでvitestを実行した結果
</font>


### GitHub Actionsで自動実行する

　まずは普通にテストを実行しようとしてみます。

```.github/workflows/test.yml
name: Vitest

on:
  pull_request_target:
    types:
      - opened
  push:
    branches:
      -

jobs:
  test:
    name: Run test codes
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Use Node.js
        uses: actions/setup-node@v4
        with:
          node-version-file: package.json
          cache: npm
          cache-dependency-path: "**/package-lock.json"
      - name: Install dependencies
        run: npm ci
      - name: build
        run: npm run build
      - name: Run Vitest
        env:
          SOMETHING_SECRET: ${{ secrets.SOMETHING_SECRET }}
        run: npm run test
```

#### DBの作成と指定

　上記のままだと、DB(SQLiteファイル)がないので落ちてしまいます。DBをCI中で作成して、それを指定してあげます。
　今回は環境変数を`cross-env`で指定しています。必要であれば

```shell
$ npm i -D cross-env
```

　を実行してください。

```diff_yaml:.github/workflows/test.yml

name: Vitest

on:
  pull_request_target:
    types:
      - opened
  push:
    branches:
      -

jobs:
  test:
    name: Run test codes
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
+      - name: Setup test DB
+        uses: cloudflare/wrangler-action@v3
+        with:
+          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+          wranglerVersion: "3.78.7"
+          command: d1 migrations apply app-stg --local --config=wrangler.test.toml
      - name: Use Node.js
        uses: actions/setup-node@v4
        with:
          node-version-file: package.json
          cache: npm
          cache-dependency-path: "**/package-lock.json"
      - name: Install dependencies
        run: npm ci
      - name: build
        run: npm run build
      - name: Run Vitest
        env:
          SOMETHING_SECRET: ${{ secrets.SOMETHING_SECRET }}
-       run: npm run test
+       run: npx cross-env SQLITE_PATH=$(find .wrangler/state/v3/d1/miniflare-D1DatabaseObject -type f -name '*.sqlite' -print -quit) npm run test
```

　`npx cross-env SQLITE_PATH=$(find .wrangler/state/v3/d1/miniflare-D1DatabaseObject -type f -name '*.sqlite' -print -quit)` によって、SQLiteファイルのパスを環境変数として渡してあげます。CI環境においては、sqliteファイルは一つしか存在しないためこれで十分です。

　VitestのCIが通るようになりました :tada: 

![CI-result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596394/e6179b1e-8408-a879-f5f8-90300a745a97.png)
<font color="#888">
VitestのCIがPassしている様子。stg環境への自動デプロイなど、他のActionもスクショに含まれている。
</font>

#### DBのキャッシュ

　上記で十分にCIは動きますが、このままだとsqliteファイルの生成とマイグレーションが毎回発生してしまいます。そこでマイグレーションファイルの更新をトリガーにSQLiteファイルの再生成を行い、それ以外はキャッシュするようにしておきます。

```diff_yaml:.github/workflows/test.yml

name: Vitest

on:
  pull_request_target:
    types:
      - opened
  push:
    branches:
      -

jobs:
  test:
    name: Run test codes
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
+      - name: cache DB
+        uses: actions/cache@v4
+        id: cache_db
+        with:
+          path: .wrangler/state/v3/d1/miniflare-D1DatabaseObject
+          key: ${{ runner.os }}-db-${{ hashFiles('repository/migrations/meta/_journal.json') }}
      - name: Setup test DB
        uses: cloudflare/wrangler-action@v3
+        if: ${{ steps.cache_db.outputs.cache-hit != 'true' }}
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          wranglerVersion: "3.78.7"
          command: d1 migrations apply app-stg --local --config=wrangler.test.toml
      - name: Use Node.js
        uses: actions/setup-node@v4
        with:
          node-version-file: package.json
          cache: npm
          cache-dependency-path: "**/package-lock.json"
      - name: Install dependencies
        run: npm ci
      - name: build
        run: npm run build
      - name: Run Vitest
        env:
          SOMETHING_SECRET: ${{ secrets.SOMETHING_SECRET }}
       run: npx cross-env SQLITE_PATH=$(find .wrangler/state/v3/d1/miniflare-D1DatabaseObject -type f -name '*.sqlite' -print -quit) npm run test
```

　GitHubのUIから、キャッシュが作成されたことを確認できました :tada: 

![cache.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596394/ca3bd280-5949-a001-e43b-2000c3855b52.png)
<font color="#888">
GitHubの`${user}/${repository}/actions/cache`からキャッシュの存在を確認
</font>
　

![using-cache.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596394/8e2a3e76-aa7e-082d-9144-89c400b8624c.png)
<font color="#888">
キャッシュがヒットし、DBのセットアップがスキップされている
</font>

## まとめ

　wranglerとvitestを用いて、D1を用いた自動テスト実行環境を整えることができました。
　みなさんも快適な自動テストライフを！