Files
waoowaoo/agent/testing.md

148 lines
4.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 测试编写详细规范
## 1. 何时必须写或更新测试
| 触发场景 | 要求 |
|---|---|
| 修改 worker handler 逻辑 | 必须有对应行为测试 |
| 修复 bug | 必须新增回归测试,`it()` 名称体现该 bug 场景 |
| 新增 API route 或 task type | 必须更新 `tests/contracts/` 矩阵 |
| 修改 prompt 后缀、referenceImages 注入、DB 写回字段 | 必须有行为断言覆盖 |
未通过 `npm run test:regression` 不得宣称功能完成。
---
## 2. 断言必须是行为级(检查具体值)
**正确写法**
```ts
// 断言 DB 写入了具体字段值
const updateData = prismaMock.globalCharacterAppearance.update.mock.calls.at(-1)?.[0].data
expect(updateData.description).toBe('AI_EXTRACTED_DESCRIPTION')
// 断言生图函数收到了正确参数
const { prompt, options } = readGenerateCall(0)
expect(prompt).toContain(CHARACTER_PROMPT_SUFFIX)
expect(options.referenceImages).toEqual(['https://ref.example/a.png'])
// 断言返回值
expect(result).toEqual({ success: true, count: 2 })
```
**禁止写法**(不能作为唯一主断言):
```ts
expect(fn).toHaveBeenCalled() // 只知道"调用了",不知道"传了什么"
expect(fn).toHaveBeenCalledTimes(1) // 次数本身无业务意义时无效
```
---
## 3. Mock 规范
**必须 Mock**
- `prisma`(所有数据库操作)
- LLM / chatCompletionWithVision / generateImage
- COS / uploadToCOS / getSignedUrl
- 外部 HTTPfetchWithTimeoutAndRetry 等)
**禁止 Mock**
- 你要测试的业务逻辑函数本身
- 项目内部常量(如 `CHARACTER_PROMPT_SUFFIX`),直接 import 使用
**禁止"自给自答"**
```ts
// 错误mock 返回 X马上断言 X没有经过任何业务逻辑
mockLLM.mockReturnValue('result')
expect(await mockLLM()).toBe('result') // 废测试
// 正确mock AI 返回 X断言业务代码把 X 写进了数据库
llmMock.getCompletionContent.mockReturnValue('高挑女性')
await handleTask(job)
expect(prismaMock.update.mock.calls.at(-1)[0].data.description).toBe('高挑女性')
```
---
## 4. 测试数据规范
- **影响分支的字段**须分开写 `it()`,例如:
- `有 extraImageUrls``无 extraImageUrls` 分别写一个用例
- `isBackgroundJob: true``false` 分别写
- **纯透传字段**`taskId``userId` 等代码不处理)可用占位值 `'task-1'`
- **每个 `it()` 命名格式**`[条件] -> [预期结果]`
**命名示例**
```
有参考图 -> AI 分析结果写入 description
无参考图 -> 不触发 AIdescription 不变
AI 调用失败 -> 主流程成功description 不被污染
缺少必填参数 -> 抛出包含字段名的错误
批量确认 2 个角色 -> 逐个处理count 返回 2
```
---
## 5. 每个测试文件的结构
```ts
// 1. vi.hoisted 定义所有 mock必须在 import 之前)
const prismaMock = vi.hoisted(() => ({ ... }))
const llmMock = vi.hoisted(() => ({ ... }))
// 2. vi.mock 注册(在 import 之前)
vi.mock('@/lib/prisma', () => ({ prisma: prismaMock }))
vi.mock('@/lib/llm-client', () => llmMock)
// 3. import 真实业务代码(在 mock 注册之后)
import { handleXxxTask } from '@/lib/workers/handlers/xxx'
// 4. describe + beforeEach 重置 mock
describe('worker xxx behavior', () => {
beforeEach(() => { vi.clearAllMocks() })
it('[条件] -> [结果]', async () => {
// 准备:覆盖这次场景需要的特殊 mock 返回值
// 构造buildJob(payload, taskType)
// 执行await handleXxxTask(job)
// 断言:检查具体值
})
})
```
---
## 6. 运行命令
| 场景 | 命令 |
|---|---|
| 改了 worker 逻辑 | `BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/worker` |
| 改了某个具体文件 | `BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/worker/xxx.test.ts` |
| 改了 API 路由 | `BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/integration/api` |
| 改了 helpers / 常量 | `BILLING_TEST_BOOTSTRAP=0 npx vitest run tests/unit/helpers` |
| 提交前完整验证 | `npm run test:regression` |
---
## 7. 目录说明
| 目录 | 用途 |
|---|---|
| `tests/unit/worker/` | worker handler 行为测试(主要回归防线) |
| `tests/unit/helpers/` | 纯函数 / 工具函数测试 |
| `tests/unit/optimistic/` | 前端状态 hook 行为测试 |
| `tests/integration/api/contract/` | API 路由契约401/400/200 + payload 断言) |
| `tests/integration/chain/` | queue → worker → 结果完整链路 |
| `tests/contracts/` | 矩阵与守卫route/tasktype/requirements |
| `tests/helpers/fakes/` | 通用 mock 工具llm、media、providers |
---
## 8. 验证测试有效性(防假绿灯)
写完测试后,用以下方式确认测试没有虚假通过:
1. 临时注释掉你刚写的业务逻辑,测试应该变红
2. 还原业务逻辑,测试应该变绿
3. 如果注释后测试还是绿,说明断言没有覆盖到真实业务路径