❤️‍🔥

Cloudflare WorkersのKVやD1のBindings取り出してテストを書く

2023/10/23に公開

Cloudflare WorkersはWorkers内で利用できるデータベースであるD1、オブジェクトストレージであるR2などのサービスに接続することができます。

ただ、通常の場合はこれらはWorkers越しに利用するものであり、開発時の確認が手間です。適当なエンドポイントを作ってPostmanで雑に叩くことで挙動確認できるものの、少し面倒です。

今回はテスト用ライブラリであるMiniflare v3を利用してKVやD1などのBindingsを取り出して、ローカルで試し書きしやすくします。

サンプルリポジトリは下記です。

https://github.com/ryokryok/cf-unit-test


試した時のスクラップは下記です。
https://zenn.dev/mr_ozin/scraps/7f80bc745c788c

準備

プロジェクトの雛形を作ってVitestの設定を行います。
wrangler initで雛形を作成していたときはテストファイルが作成されていましたが、新しいコマンドの方では作成されないため、自分で作成する必要があります。

pnpm create cloudflare@latest cf-unit-test
## 色々聞かれるがtype "Hello World" WorkerとTypeScript使えばOK
cd cf-unit-test

テスト用に必要なライブラリのinstallとテストファイル作成。

MiniflareによってWorkerのランタイムが起動されて実行できます。

pnpm i -D vitest miniflare
touch src/index.test.ts
src/index.test.ts
import { it, expect } from 'vitest';
import { Miniflare } from 'miniflare';

it('worker test', async () => {
  const mf = new Miniflare({
    name: 'main',
    modules: true,
    script: `
    export default {
      async fetch(request, env, ctx){
        return new Response('Hello World!');
      },};
    `,
  });

  const res = await mf.dispatchFetch('http://localhost:8787/');
  expect(await res.text()).toBe('Hello World!');
});

テスト実行。今回はデフォルト設定で動作するので、vitest.config.tsは無しでOK。

pnpm vitest

実行後、テストがPassすることを確認します。

Miniflareから各種Bindingオブジェクトを取り出す

ここからが本題です。MiniflareでKVやD1のテストを書く場合はMiniflareインスタンス作成時にBindingの設定を行い、mf.get**で取り出すだけでBinding単体を取り出せます。
この際にWranglerコマンドの実行や、wrangler.tomlへの記載も不要です。

scriptの内容についてはWorkerにFetchする毎に変わる処理がなければなんでもOKです。

src/index.test.ts
it('storage test', async () => {
  const mf = new Miniflare({
    name: 'main',
    modules: true,
    script: `
    export default {
      async fetch(request, env, ctx){
        return new Response('Hello World!');
      },};
    `,
    kvNamespaces: ['KV'],
    d1Databases: ['DB'],
    r2Buckets: ['BUCKET'],
  });

  const kv = await mf.getKVNamespace('KV');
  await kv.put('foo', 'bar');
  expect(await kv.get('foo')).toBe('bar');

  const d1 = await mf.getD1Database('DB');
  await d1.exec('DROP TABLE IF EXISTS Users;');
  await d1.exec('CREATE TABLE IF NOT EXISTS Users (ID INTEGER PRIMARY KEY, Name TEXT, Email TEXT);');
  await d1.exec(`INSERT INTO Users (ID, Name, Email) VALUES (1, 'Tom', 'tom@example.com');`);
  const { results } = await d1.prepare('SELECT * FROM Users WHERE ID = ?').bind(1).all();
  expect(results).toEqual([
    {
      ID: 1,
      Name: 'Tom',
      Email: 'tom@example.com',
    },
  ]);

  const r2 = await mf.getR2Bucket('BUCKET');
  await r2.put('foo', 'bar');
  const object = await r2.get('foo');
  expect(await object?.text()).toBe('bar');
});

対応しているBindngsはCache,Durable Objects,KV,R2,D1,Queuesのようです。

https://github.com/cloudflare/miniflare/blob/v3.20231016.0/packages/miniflare/README.md#interface-workeroptions

感想

Cloudflare Workersはテスト面がやりづらいと聞いていましたが、MiniflareがうまいことMockしてくれるので、Workersで行う処理を関数で切り出して、テストを書いて確認するようなことがやりやすくなりました。

ただ現時点ではMiniflare v3がGitHubのREADME.mdぐらいしかないので、またコロコロ変わりそうだなとは思っています。

Discussion