mcpField
C
cloudflare-workers-testing

cloudflare-workers-testing

github.com/secondskyテスト・品質240330 回インストール5 週間前に更新
リポジトリ

cloudflare-workers-testing について

Cloudflare Workers 開発者向けの Vitest テストガイドです。Miniflare で Workers バインディングと分離ストレージを使う単体・統合テストを解説します。

主な機能

  1. 01Miniflare でのテスト実行
  2. 02D1、KV、R2 などのバインディングをテスト
  3. 03実行コンテキストとバックグラウンド処理を検証
  4. 04カバレッジ追跡の設定
  5. 05単体・統合テストのパターンを解説

活用シーン

  1. 01D1 を使う API のテスト
  2. 02KV キャッシュ動作の検証
  3. 03R2 ファイルアップロードのテスト
  4. 04Durable Objects の状態確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/secondsky/claude-skills --skill cloudflare-workers-testing

インストール後、Claude Code で/cloudflare-workers-testingを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
binding-mocks.md
coverage-optimization.md
integration-testing.md
troubleshooting.md
vitest-setup.md
scripts/
run-tests.sh
setup-vitest.sh
templates/
basic-test.ts
binding-mock-test.ts
vitest-config.ts

詳細

Vitest を使用した Cloudflare Workers のテスト

ステータス:✅ 本番環境で使用可能 | 最終確認日:2025-01-27 Vitest:^2.0.0 | @cloudflare/vitest-pool-workers:0.7.2 | Miniflare:最新

目次


Workers のテストとは?

Vitest と @cloudflare/vitest-pool-workers を使用して Cloudflare Workers をテストすると、すべてのバインディング(D1、KV、R2、Durable Objects、Queues、AI)を完全にサポートする実際の Workers 環境で、ユニットテストや統合テストを作成して実行できます。テストはローカル開発では Miniflare 上で実行され、CI/CD では実際の Workers ランタイムの動作で実行できます。

主な機能:バインディングのモック、実行コンテキストのテスト、エッジランタイムのシミュレーション、カバレッジの追跡、高速なテスト実行。


2025 年の新機能

@cloudflare/vitest-pool-workers 0.7.2(2025 年 1 月):

  • 破壊的変更:Miniflare v3 → Node.js 20 以降が必要
  • 新機能:env/ctx にアクセスするための cloudflare:test モジュール
  • 改善:バインディング用の分離ストレージが高速化
  • 修正:Worker 間のサービスバインディングが正常に動作
  • 追加:Vectorize および Workers AI バインディングのサポート

以前のバージョンからの移行:

# Update dependencies
bun add -D vitest@^2.1.8 @cloudflare/vitest-pool-workers@^0.7.2

# Update vitest.config.ts (new pool configuration format)
export default defineWorkersConfig({
  test: {
    poolOptions: {
      workers: {
        wrangler: { configPath: './wrangler.jsonc' },
        miniflare: { compatibilityDate: '2025-01-27' }
      }
    }
  }
});

クイックスタート(5 分)

1. 依存関係をインストールする
bun add -D vitest @cloudflare/vitest-pool-workers
2. vitest.config.ts を作成する
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';

export default defineWorkersConfig({
  test: {
    poolOptions: {
      workers: {
        wrangler: { configPath: './wrangler.jsonc' },
        miniflare: {
          compatibilityDate: '2025-01-27',
          compatibilityFlags: ['nodejs_compat']
        }
      }
    }
  }
});
3. 最初のテストを書く
import { describe, it, expect } from 'vitest';
import { env, createExecutionContext, waitOnExecutionContext } from 'cloudflare:test';
import worker from '../src/index';

describe('Worker', () => {
  it('responds with 200', async () => {
    const request = new Request('http://example.com/');
    const ctx = createExecutionContext();
    const response = await worker.fetch(request, env, ctx);
    await waitOnExecutionContext(ctx);

    expect(response.status).toBe(200);
  });
});
4. テストを実行する
bun test
# or
bunx vitest

重要なルール

1. env へのアクセスには必ず cloudflare:test を使用する

✅ 正しい例:

import { env } from 'cloudflare:test';

it('queries D1', async () => {
  const result = await env.DB.prepare('SELECT * FROM users').all();
  expect(result.results).toHaveLength(0); // Fresh isolated DB per test
});

❌ 誤った例:

// Don't manually create env object
const env = { DB: mockDB }; // ❌ Won't use real D1 binding

理由:cloudflare:test は、wrangler.jsonc で設定された実際のバインディングを提供し、テストごとにストレージを分離します。

2. 実行コンテキストの完了を必ず待つ

✅ 正しい例:

it('handles async operations', async () => {
  const ctx = createExecutionContext();
  const response = await worker.fetch(request, env, ctx);
  await waitOnExecutionContext(ctx); // ✅ Ensures ctx.waitUntil completes

  expect(response.status).toBe(200);
});

❌ 誤った例:

it('missing wait', async () => {
  const ctx = createExecutionContext();
  const response = await worker.fetch(request, env, ctx);
  // ❌ Missing waitOnExecutionContext - ctx.waitUntil tasks may not complete
  expect(response.status).toBe(200);
});

理由:Workers はバックグラウンドタスク(ロギング、分析など)に ctx.waitUntil() を使用します。完了を待たないと、テスト内でこれらのタスクが完了しないことがあります。

3. 各テストには分離されたストレージが割り当てられる

✅ 正しい例:

describe('KV Operations', () => {
  it('test 1: writes to KV', async () => {
    await env.CACHE.put('key', 'value1');
    const val = await env.CACHE.get('key');
    expect(val).toBe('value1'); // ✅ Isolated
  });

  it('test 2: clean state', async () => {
    const val = await env.CACHE.get('key');
    expect(val).toBeNull(); // ✅ Test 1's data doesn't leak here
  });
});

理由:各テストは、新しいバインディングストレージを使用して実行されます(分離は自動的に行われます)。

4. バインディングには Wrangler の設定を使用する

✅ 正しい例:

// vitest.config.ts
export default defineWorkersConfig({
  test: {
    poolOptions: {
      workers: {
        wrangler: { configPath: './wrangler.jsonc' } // ✅ Reads bindings from wrangler
      }
    }
  }
});

❌ 誤った例:

// vitest.config.ts
export default defineWorkersConfig({
  test: {
    poolOptions: {
      workers: {
        // ❌ No wrangler config - bindings won't be available
        miniflare: { compatibilityDate: '2025-01-27' }
      }
    }
  }
});

理由:Wrangler の設定で、すべてのバインディング(D1、KV、R2 など)を定義します。設定がないと、env は空になります。

5. 互換性日付を一致させる

✅ 正しい例:

// vitest.config.ts
miniflare: {
  compatibilityDate: '2025-01-27' // ✅ Matches wrangler.jsonc
}

// wrangler.jsonc
{
  "compatibility_date": "2025-01-27"
}

理由:テスト環境が本番ランタイムの動作と一致するようにします。


基本概念

バインディングのテストパターン

D1 データベース:

import { env } from 'cloudflare:test';

it('queries D1', async () => {
  // Insert test data
  await env.DB.prepare('INSERT INTO users (name) VALUES (?)').bind('Alice').run();

  // Query
  const result = await env.DB.prepare('SELECT * FROM users WHERE name = ?').bind('Alice').first();
  expect(result?.name).toBe('Alice');
});

KV ネームスペース:

it('reads from KV', async () => {
  await env.CACHE.put('test-key', 'test-value');
  const value = await env.CACHE.get('test-key');
  expect(value).toBe('test-value');
});

R2 バケット:

it('uploads to R2', async () => {
  await env.BUCKET.put('file.txt', 'Hello World');
  const object = await env.BUCKET.get('file.txt');
  expect(await object?.text()).toBe('Hello World');
});

Durable Objects:

it('interacts with Durable Object', async () => {
  const id = env.COUNTER.idFromName('test-counter');
  const stub = env.COUNTER.get(id);

  const response = await stub.fetch('http://fake/increment');
  const data = await response.json();
  expect(data.count).toBe(1);
});
ユニットテストと統合テスト

ユニットテスト(単一の関数):

import { validateInput } from '../src/utils/validator';

it('validates input', () => {
  const result = validateInput({ name: 'Alice', age: 30 });
  expect(result.valid).toBe(true);
});

統合テスト(fetch ハンドラー全体):

import worker from '../src/index';
import { env, createExecutionContext, waitOnExecutionContext } from 'cloudflare:test';

it('handles full request flow', async () => {
  const request = new Request('http://example.com/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Alice' })
  });

  const ctx = createExecutionContext();
  const response = await worker.fetch(request, env, ctx);
  await waitOnExecutionContext(ctx);

  expect(response.status).toBe(201);
  const user = await response.json();
  expect(user.name).toBe('Alice');
});
カバレッジの設定

vitest.config.ts に追加します:

export default defineWorkersConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      include: ['src/**/*.ts'],
      exclude: ['src/**/*.test.ts', 'src/**/*.spec.ts'],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 80,
        statements: 80
      }
    }
  }
});

カバレッジ付きで実行します:

bunx vitest run --coverage

主な使用例 5 選

1. D1 を使用した API エンドポイントのテスト
it('creates user via API', async () => {
  const request = new Request('http://example.com/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Bob', email: '[email protected]' })
  });

  const ctx = createExecutionContext();
  const response = await worker.fetch(request, env, ctx);
  await waitOnExecutionContext(ctx);

  expect(response.status).toBe(201);

  // Verify DB insert
  const user = await env.DB.prepare('SELECT * FROM users WHERE email = ?')
    .bind('[email protected]')
    .first();
  expect(user?.name).toBe('Bob');
});
2. KV を使用したキャッシュのテスト
it('caches API responses', async () => {
  // First request (cache miss)
  const req1 = new Request('http://example.com/api/data');
  const ctx1 = createExecutionContext();
  const res1 = await worker.fetch(req1, env, ctx1);
  await waitOnExecutionContext(ctx1);

  expect(res1.headers.get('X-Cache')).toBe('MISS');

  // Second request (cache hit)
  const req2 = new Request('http://example.com/api/data');
  const ctx2 = createExecutionContext();
  const res2 = await worker.fetch(req2, env, ctx2);
  await waitOnExecutionContext(ctx2);

  expect(res2.headers.get('X-Cache')).toBe('HIT');
});
3. R2 へのファイルアップロードのテスト
it('handles file upload', async () => {
  const formData = new FormData();
  formData.append('file', new Blob(['test content'], { type: 'text/plain' }), 'test.txt');

  const request = new Request('http://example.com/upload', {
    method: 'POST',
    body: formData
  });

  const ctx = createExecutionContext();
  const response = await worker.fetch(request, env, ctx);
  await waitOnExecutionContext(ctx);

  expect(response.status).toBe(200);

  // Verify R2 upload
  const object = await env.BUCKET.get('test.txt');
  expect(await object?.text()).toBe('test content');
});
4. Durable Objects の状態のテスト
it('maintains counter state', async () => {
  const id = env.COUNTER.idFromName('my-counter');
  const stub = env.COUNTER.get(id);

  // Increment 3 times
  for (let i = 0; i < 3; i++) {
    await stub.fetch('http://fake/increment');
  }

  // Verify state
  const response = await stub.fetch('http://fake/value');
  const data = await response.json();
  expect(data.count).toBe(3);
});
5. キューコンシューマーのテスト
it('processes queue messages', async () => {
  const messages = [
    { id: '1', body: { action: 'email', to: '[email protected]' }, timestamp: new Date() }
  ];

  // Simulate queue batch
  await worker.queue(
    {
      queue: 'my-queue',
      messages,
      retryAll: () => {},
      ackAll: () => {}
    },
    env
  );

  // Verify processing (check DB, logs, etc.)
  const log = await env.DB.prepare('SELECT * FROM email_log WHERE id = ?').bind('1').first();
  expect(log?.status).toBe('sent');
});

ベストプラクティス

✅ 推奨事項
  1. 内容がわかるテスト名を付ける:

    it('returns 404 when user not found', async () => {});
    it('validates email format before saving', async () => {});
    
  2. エラーケースをテストする:

    it('returns 400 for invalid JSON', async () => {
      const request = new Request('http://example.com/api', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: 'invalid json'
      });
    
      const ctx = createExecutionContext();
      const response = await worker.fetch(request, env, ctx);
      await waitOnExecutionContext(ctx);
    
      expect(response.status).toBe(400);
    });
    
  3. 関連するテストをグループ化する:

    describe('User API', () => {
      describe('POST /users', () => {
        it('creates user with valid data', async () => {});
        it('rejects duplicate email', async () => {});
        it('validates required fields', async () => {});
      });
    });
    
  4. セットアップには beforeEach を使用する:

    describe('Database tests', () => {
      beforeEach(async () => {
        // Seed test data
        await env.DB.prepare('INSERT INTO users (name) VALUES (?)').bind('Test User').run();
      });
    
      it('queries users', async () => {
        const result = await env.DB.prepare('SELECT * FROM users').all();
        expect(result.results).toHaveLength(1);
      });
    });
    
  5. 現実的なシナリオをテストする:

    it('handles concurrent requests', async () => {
      const requests = Array(10).fill(null).map(() =>
        worker.fetch(new Request('http://example.com/'), env, createExecutionContext())
      );
    
      const responses = await Promise.all(requests);
      expect(responses.every(r => r.status === 200)).toBe(true);
    });
    
❌ 避けること
  1. テスト間で状態を共有しない:

    // ❌ BAD: Leaky state
    let counter = 0;
    it('test 1', () => { counter++; });
    it('test 2', () => { expect(counter).toBe(1); }); // Fragile!
    
    // ✅ GOOD: Isolated
    it('test 1', () => { const counter = 0; counter++; });
    it('test 2', () => { const counter = 0; /* fresh state */ });
    
  2. 完了を待ち忘れない:

    // ❌ BAD
    const response = await worker.fetch(request, env, ctx);
    expect(response.status).toBe(200); // ctx.waitUntil not finished
    
    // ✅ GOOD
    const response = await worker.fetch(request, env, ctx);
    await waitOnExecutionContext(ctx);
    expect(response.status).toBe(200);
    
  3. URL をハードコードしない:

    // ❌ BAD
    const request = new Request('http://example.com/test');
    
    // ✅ GOOD
    const request = new Request('http://fake-host/test'); // Host doesn't matter in tests
    
  4. 実装の詳細をテストしない:

    // ❌ BAD: Testing internals
    expect(worker.privateHelperFunction).toBeDefined();
    
    // ✅ GOOD: Testing behavior
    const response = await worker.fetch(request, env, ctx);
    expect(response.status).toBe(200);
    

防止できる主なエラー 8 選

1. ❌ ReferenceError: env is not defined

原因:cloudflare:test から env をインポートしていません。

修正方法:

import { env } from 'cloudflare:test'; // ✅ Add this import

予防策:env へのアクセスには必ず cloudflare:test モジュールを使用します。


2. ❌ TypeError: Cannot read property 'DB' of undefined

原因:vitest.config.ts で wrangler.jsonc が読み込まれていません。

修正方法:

export default defineWorkersConfig({
  test: {
    poolOptions: {
      workers: {
        wrangler: { configPath: './wrangler.jsonc' } // ✅ Add this
      }
    }
  }
});

予防策:必ず Vitest の設定で Wrangler のパスを設定してください。


3. ❌ Error: D1_ERROR: no such table: users

原因:テストで D1 データベースのスキーマが適用されていません。

修正方法:

// Option 1: Seed in beforeEach
beforeEach(async () => {
  await env.DB.exec(`
    CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY,
      name TEXT NOT NULL
    )
  `);
});

// Option 2: Use migrations (load from file)
beforeEach(async () => {
  const schema = await fs.readFile('./migrations/schema.sql', 'utf-8');
  await env.DB.exec(schema);
});

予防策:各テストの前にスキーマを作成するか、共有セットアップを使用してください。


4. ❌ Error: ctx.waitUntil tasks did not complete

原因:await waitOnExecutionContext(ctx) がありません。

修正方法:

const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await waitOnExecutionContext(ctx); // ✅ Add this

予防策:テストでは必ず実行コンテキストの完了を待ってください。


5. ❌ Error: SELF is not defined

原因:ワーカーを直接インポートする代わりに、古い SELF.fetch() パターンを使用しています。

修正方法:

// ❌ OLD (vitest-pool-workers <0.5)
import { SELF } from 'cloudflare:test';
await SELF.fetch(request);

// ✅ NEW (vitest-pool-workers ≥0.7)
import worker from '../src/index';
await worker.fetch(request, env, ctx);

予防策:ワーカーを直接インポートしてください(最新のパターン)。


6. ❌ Error: KV.get() returned data from previous test

原因:ストレージが共有されていると思い込んでいます(実際には共有されませんが、テスト間のデータ漏れを示している可能性があります)。

修正方法:各テストは分離されています。このエラーが発生する場合は、次の点を確認してください:

// ❌ Test pollution (shared variable)
let cache = {};
it('test 1', () => { cache.key = 'value'; });
it('test 2', () => { expect(cache.key).toBeUndefined(); }); // Fails!

// ✅ Proper isolation
it('test 1', async () => { await env.CACHE.put('key', 'value1'); });
it('test 2', async () => { const val = await env.CACHE.get('key'); expect(val).toBeNull(); });

予防策:テストデータに共有変数を使用しないでください。


7. ❌ TypeError: env.BUCKET.put is not a function

原因:wrangler.jsonc で R2 バインディングが設定されていません。

修正方法:

// wrangler.jsonc
{
  "r2_buckets": [
    { "binding": "BUCKET", "bucket_name": "test-bucket" }
  ]
}

予防策:Wrangler の設定で、すべてのバインディングを定義してください。


8. ❌ Error: Pool 'workers' is not supported

原因:@cloudflare/vitest-pool-workers の依存関係がありません。

修正方法:

bun add -D @cloudflare/vitest-pool-workers

予防策:Workers のテスト用にプールパッケージをインストールしてください。


リファレンスを読み込むタイミング

詳細な専門情報については、リファレンスファイルを読み込んでください。

次の場合は references/vitest-setup.md を読み込んでください:

  • Vitest をゼロからセットアップする
  • カスタムプールオプションを設定する
  • Miniflare の設定をトラブルシューティングする
  • 古いバージョンの vitest-pool-workers から移行する

次の場合は references/binding-mocks.md を読み込んでください:

  • 特定のバインディング(D1、KV、R2、DO、Queues、AI、Vectorize)をテストする
  • サービスバインディング(ワーカー間)をモックする
  • バインディング用のテストフィクスチャを作成する
  • 分離されたストレージの動作を理解する

次の場合は references/integration-testing.md を読み込んでください:

  • リクエスト/レスポンスの包括的なテストを作成する
  • 複数ステップのワークフローをテストする
  • 本番環境のシナリオをシミュレートする
  • WebSocket またはストリーミングレスポンスをテストする

次の場合は references/coverage-optimization.md を読み込んでください:

  • カバレッジのしきい値を設定する
  • テストされていないコードパスを特定する
  • テストスイートのパフォーマンスを最適化する
  • カバレッジレポーターを設定する

次の場合は references/troubleshooting.md を読み込んでください:

  • 失敗するテストをデバッグする
  • バインディングエラーを解決する
  • タイムアウトの問題を修正する
  • エラーメッセージを理解する

次の用途には templates/vitest-config.ts を読み込んでください:

  • vitest.config.ts の完全な例
  • 高度な設定オプション
  • 複数の Wrangler 環境

次の用途には templates/basic-test.ts を読み込んでください:

  • テストファイル構成のテンプレート
  • 一般的なテストパターン
  • beforeEach/afterEach の例

次の用途には templates/binding-mock-test.ts を読み込んでください:

  • バインディング固有のテスト例
  • D1、KV、R2、DO のテストパターン
  • Queue と AI のテスト例

次の用途には scripts/setup-vitest.sh を読み込んでください:

  • Vitest の自動インストール
  • プロジェクト設定スクリプト

次の用途には scripts/run-tests.sh を読み込んでください:

  • CI/CD でのテスト実行
  • カバレッジレポートの自動化

関連する Cloudflare プラグイン

サービス固有のテストパターンについては、次を読み込んでください:

  • cloudflare-d1 - D1 データベースのテスト、マイグレーション、データ投入
  • cloudflare-kv - KV ネームスペースのテスト、TTL の検証
  • cloudflare-r2 - R2 バケットのテスト、ファイルのアップロード/ダウンロード
  • cloudflare-durable-objects - DO のテスト、WebSocket のテスト
  • cloudflare-queues - Queue のテスト、バッチ処理
  • cloudflare-workers-ai - AI モデルのテスト、推論のモック

このスキルでは、すべてのバインディングの種類と Workers の機能に適用できる、横断的な Workers のテストパターンを扱います。


質問がありますか? references/troubleshooting.md を読み込むか、/workers-debug コマンドを使用して対話形式でサポートを受けてください。

コメント