アプリケーションを作成するとき、やらなきゃいけないのは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
- wranglerのunstable_dev
- miniflareを使う
最新はvitest-pool-workers
ですが、バージョンが0系であることもあり、まだまだ不安定です。私の環境ではバグを踏んでうまく動きませんでした。よって、unstable_dev
を用いる方法でテストを書いています。
書いていく
基本的な流れは
-
unstable_dev
でのwrangler立ち上げ - テスト実行
- wrangler終了
ですね。
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、分けたいですよねぇ)
テストコードは下記のように書きます。
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
を使うように設定しておきます。
SOMETHING_SECRET=very-important-secret
import dotenv from "dotenv";
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
env: dotenv.config({ path: ".env.test" }).parsed,
},
});
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,
},
+ 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ファイルが生成されます。こちらを後ほど使用しますので、環境変数に設定しておきます。
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に接続していきます。
+ 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を操作できます。
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のテスト実行ができるようになりました
GitHub Actionsで自動実行する
まずは普通にテストを実行しようとしてみます。
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
を実行してください。
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が通るようになりました
VitestのCIがPassしている様子。stg環境への自動デプロイなど、他のActionもスクショに含まれている。
DBのキャッシュ
上記で十分にCIは動きますが、このままだとsqliteファイルの生成とマイグレーションが毎回発生してしまいます。そこでマイグレーションファイルの更新をトリガーにSQLiteファイルの再生成を行い、それ以外はキャッシュするようにしておきます。
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から、キャッシュが作成されたことを確認できました
GitHubの${user}/${repository}/actions/cache
からキャッシュの存在を確認
キャッシュがヒットし、DBのセットアップがスキップされている
まとめ
wranglerとvitestを用いて、D1を用いた自動テスト実行環境を整えることができました。
みなさんも快適な自動テストライフを!