OPC Stack 使用 Vitest 进行测试,支持单元测试和 E2E 测试。
单元测试
测试文件
单元测试文件放在 src/**/*.test.ts。
// src/api/handler/users.test.ts
import { describe, it, expect } from 'vitest'
import { app } from './users'
describe('Users API', () => {
it('should get user by id', async () => {
const res = await app.request('/api/users/123')
expect(res.status).toBe(200)
const data = await res.json()
expect(data.id).toBe('123')
})
it('should return 404 for non-existent user', async () => {
const res = await app.request('/api/users/999')
expect(res.status).toBe(404)
})
})
运行测试
# 运行所有测试
pnpm test
# 运行单个文件
pnpm test src/api/handler/users.test.ts
# 监听模式
pnpm test --watch
E2E 测试
测试文件
E2E 测试文件放在 e2e/**/*.test.ts。
// e2e/auth.test.ts
import { describe, it, expect, beforeAll } from 'vitest'
import { createTestClient } from './utils'
describe('Auth E2E', () => {
let client: ReturnType<typeof createTestClient>
beforeAll(() => {
client = createTestClient()
})
it('should sign up with email', async () => {
const res = await client.post('/api/auth/signup', {
email: 'test@example.com',
password: 'password123'
})
expect(res.status).toBe(200)
expect(res.data.token).toBeDefined()
})
it('should sign in with email', async () => {
const res = await client.post('/api/auth/signin', {
email: 'test@example.com',
password: 'password123'
})
expect(res.status).toBe(200)
expect(res.data.token).toBeDefined()
})
})
运行 E2E 测试
# 运行 E2E 测试
pnpm test:e2e
# 监听模式
pnpm test:e2e --watch
BDD 风格测试
OPC Stack 提供 BDD 风格的测试工具,参考 src/testing/bdd.ts。
Mock 数据
Mock D1
import { describe, it, expect, vi } from 'vitest'
describe('Database operations', () => {
it('should query users', async () => {
const mockDB = {
query: {
users: {
findFirst: vi.fn().mockResolvedValue({
id: '123',
email: 'test@example.com'
})
}
}
}
const user = await mockDB.query.users.findFirst()
expect(user.id).toBe('123')
})
})
Mock R2
import { describe, it, expect, vi } from 'vitest'
describe('File operations', () => {
it('should upload file', async () => {
const mockR2 = {
put: vi.fn().mockResolvedValue(undefined)
}
await mockR2.put('public/logo.png', new Blob())
expect(mockR2.put).toHaveBeenCalledWith(
'public/logo.png',
expect.any(Blob)
)
})
})
测试覆盖率
# 生成覆盖率报告
pnpm test --coverage
# 查看报告
open coverage/index.html
测试环境
配置测试环境变量
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
env: {
NODE_ENV: 'test',
DATABASE_URL: ':memory:',
OPENAI_API_KEY: 'test-key'
}
}
})
使用测试数据库
import { beforeEach, afterEach } from 'vitest'
import { drizzle } from 'drizzle-orm/d1'
let db: ReturnType<typeof drizzle>
beforeEach(async () => {
// 创建测试数据库
db = drizzle(env.DB)
await db.run(sql`CREATE TABLE users (...)`)
})
afterEach(async () => {
// 清理测试数据
await db.run(sql`DROP TABLE users`)
})
最佳实践
- 测试隔离:每个测试应该独立,不依赖其他测试
- 清理数据:测试后清理创建的数据
- Mock 外部服务:不要在测试中调用真实的外部 API
- 测试边界条件:测试正常情况和异常情况
- 保持简单:测试代码应该简单易懂
常见问题
Q: 测试运行很慢?
使用 --run 标志运行单次测试,避免监听模式。
Q: 如何测试需要认证的 API?
在测试中生成测试 token,或者 mock 认证中间件。
Q: 如何测试 Cron 和 Queue?
直接调用 handler 函数,传入 mock 的 event 和 env。