mcpField
C
cloudflare-workers-testing

cloudflare-workers-testing

github.com/secondsky测试与质量240330 次安装5 周前更新
代码仓库

关于 cloudflare-workers-testing

面向 Cloudflare Workers 开发者的 Vitest 测试指南,介绍如何在 Miniflare 中运行单元和集成测试,并使用 Workers 绑定及隔离存储。

主要功能

  1. 01在 Miniflare 中运行测试
  2. 02测试 D1、KV、R2 等绑定
  3. 03验证执行上下文和后台任务
  4. 04配置覆盖率追踪
  5. 05讲解单元与集成测试模式

适用场景

  1. 01测试使用 D1 的 API
  2. 02验证 KV 缓存行为
  3. 03测试 R2 文件上传
  4. 04检查 Durable 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,可以编写在真实 Workers 环境中运行的单元测试和集成测试,并完整支持绑定(D1、KV、R2、Durable Objects、Queues、AI)。测试在 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. 始终使用 cloudflare:test 访问 Env

✅ 正确:

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

预防方法:始终使用 cloudflare:test 模块访问 env。


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() 模式,而不是直接导入 Worker。

修复:

// ❌ 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);

预防:直接导入 Worker(现代模式)。


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 测试所需的 pool 包。


何时加载参考资料

需要详细的专项内容时,加载参考文件:

在以下情况下加载 references/vitest-setup.md:

  • 从头开始设置 Vitest
  • 配置自定义 pool 选项
  • 排查 Miniflare 配置问题
  • 从较旧版本的 vitest-pool-workers 迁移

在以下情况下加载 references/binding-mocks.md:

  • 测试特定绑定(D1、KV、R2、DO、Queues、AI、Vectorize)
  • 模拟服务绑定(Worker 间调用)
  • 为绑定创建测试夹具
  • 了解隔离存储的行为

在以下情况下加载 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 命令获取交互式帮助。

评论

支持 Markdown