3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-09-22

 アプリケーションを作成するとき、やらなきゃいけないのはCI / CD環境の構築ですよね。
 Cloudflareで作成したアプリの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ですが、バージョンが0系であることもあり、まだまだ不安定です。私の環境ではバグを踏んでうまく動きませんでした。よって、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を使うように設定しておきます。

.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にも変更が必要です。

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を作っていきます。

$ wrangler d1 create app-stg --config=wrangler.test.toml

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

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

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

$ npm i -D better-sqlite3

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

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 をローカル環境に用意してマイグレーションをする必要があります。

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

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

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

vitest-local.png

ローカル環境のCUIでvitestを実行した結果

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で指定しています。必要であれば

$ npm i -D cross-env

 を実行してください。

.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

VitestのCIがPassしている様子。stg環境への自動デプロイなど、他のActionもスクショに含まれている。

DBのキャッシュ

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

.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

GitHubの${user}/${repository}/actions/cacheからキャッシュの存在を確認

 

using-cache.png

キャッシュがヒットし、DBのセットアップがスキップされている

まとめ

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

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?