specops 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/agent/demand-analyst.md +69 -18
- package/.opencode/agent/verifier.md +231 -0
- package/.opencode/skills/brainstorming/SKILL.md +105 -0
- package/.opencode/skills/demand-analysis/SKILL.md +261 -47
- package/.opencode/skills/dispatching-parallel-agents/SKILL.md +180 -0
- package/.opencode/skills/executing-plans/SKILL.md +90 -0
- package/.opencode/skills/finishing-a-development-branch/SKILL.md +222 -0
- package/.opencode/skills/receiving-code-review/SKILL.md +213 -0
- package/.opencode/skills/repo-clone-analyze/SKILL.md +371 -0
- package/.opencode/skills/requesting-code-review/SKILL.md +105 -0
- package/.opencode/skills/requesting-code-review/code-reviewer.md +146 -0
- package/.opencode/skills/subagent-driven-development/SKILL.md +242 -0
- package/.opencode/skills/subagent-driven-development/code-quality-reviewer-prompt.md +20 -0
- package/.opencode/skills/subagent-driven-development/implementer-prompt.md +78 -0
- package/.opencode/skills/subagent-driven-development/spec-reviewer-prompt.md +61 -0
- package/.opencode/skills/systematic-debugging/SKILL.md +296 -0
- package/.opencode/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/.opencode/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/.opencode/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/.opencode/skills/test-driven-development/SKILL.md +399 -0
- package/.opencode/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/.opencode/skills/using-git-worktrees/SKILL.md +218 -0
- package/.opencode/skills/using-superpowers/SKILL.md +99 -0
- package/.opencode/skills/verification-before-completion/SKILL.md +150 -0
- package/.opencode/skills/writing-plans/SKILL.md +123 -0
- package/.opencode/skills/writing-skills/SKILL.md +654 -0
- package/dist/__e2e__/01-state-engine.e2e.test.js +1 -1
- package/dist/acceptance/lazyDetector.js +1 -1
- package/dist/acceptance/lazyDetector.test.js +1 -1
- package/dist/acceptance/reporter.js +1 -1
- package/dist/acceptance/reporter.test.js +1 -1
- package/dist/acceptance/runner.js +1 -1
- package/dist/acceptance/runner.test.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/promptTemplate.js +1 -1
- package/dist/context/promptTemplate.test.js +1 -1
- package/dist/context/techContextLoader.js +1 -1
- package/dist/context/techContextLoader.test.js +1 -1
- package/dist/engine.js +1 -1
- package/dist/evolution/distiller.js +1 -1
- package/dist/evolution/index.js +1 -1
- package/dist/evolution/memoryGraph.js +1 -1
- package/dist/evolution/selector.js +1 -1
- package/dist/evolution/signals.js +1 -1
- package/dist/evolution/solidify.js +1 -1
- package/dist/evolution/store.js +1 -1
- package/dist/evolution/types.js +1 -1
- package/dist/init.js +1 -1
- package/dist/machines/agentMachine.js +1 -1
- package/dist/machines/agentMachine.test.js +1 -1
- package/dist/machines/supervisorMachine.js +1 -1
- package/dist/machines/supervisorMachine.test.js +1 -1
- package/dist/persistence/schema.js +1 -1
- package/dist/persistence/stateFile.js +1 -1
- package/dist/persistence/stateFile.test.js +1 -1
- package/dist/plugin-engine.js +1 -1
- package/dist/plugin.js +1 -1
- package/dist/types/index.js +1 -1
- package/dist/utils/id.js +1 -1
- package/package.json +8 -2
- package/scripts/postinstall.mjs +37 -7
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-driven-development
|
|
3
|
+
description: 实现任何功能或修复bug之前使用,在编写实现代码之前
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 测试驱动开发(TDD)
|
|
7
|
+
|
|
8
|
+
## 概述
|
|
9
|
+
|
|
10
|
+
先写测试。看它失败。写最少的代码让它通过。
|
|
11
|
+
|
|
12
|
+
**核心原则:** 如果你没有看到测试失败,你就不知道它是否测试了正确的东西。
|
|
13
|
+
|
|
14
|
+
**违反规则的字面意思就是违反规则的精神。**
|
|
15
|
+
|
|
16
|
+
## 何时使用
|
|
17
|
+
|
|
18
|
+
**始终使用:**
|
|
19
|
+
- 新功能
|
|
20
|
+
- Bug 修复
|
|
21
|
+
- 重构
|
|
22
|
+
- 行为变更
|
|
23
|
+
|
|
24
|
+
**例外(需征得你的人类搭档同意):**
|
|
25
|
+
- 一次性原型
|
|
26
|
+
- 生成的代码
|
|
27
|
+
- 配置文件
|
|
28
|
+
|
|
29
|
+
想着"就这一次跳过 TDD"?停下来。那是自我合理化。
|
|
30
|
+
|
|
31
|
+
## 铁律
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
没有先写失败测试,就不能写生产代码
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
先写了代码再写测试?删掉。从头来过。
|
|
38
|
+
|
|
39
|
+
**没有例外:**
|
|
40
|
+
- 不要把它留作"参考"
|
|
41
|
+
- 不要在写测试时"改编"它
|
|
42
|
+
- 不要看它
|
|
43
|
+
- 删除就是删除
|
|
44
|
+
|
|
45
|
+
从测试出发,重新实现。句号。
|
|
46
|
+
|
|
47
|
+
## 红-绿-重构
|
|
48
|
+
|
|
49
|
+
```dot
|
|
50
|
+
digraph tdd_cycle {
|
|
51
|
+
rankdir=LR;
|
|
52
|
+
red [label="红灯\n写失败测试", shape=box, style=filled, fillcolor="#ffcccc"];
|
|
53
|
+
verify_red [label="验证失败\n是否正确", shape=diamond];
|
|
54
|
+
green [label="绿灯\n最少代码", shape=box, style=filled, fillcolor="#ccffcc"];
|
|
55
|
+
verify_green [label="验证通过\n全部绿灯", shape=diamond];
|
|
56
|
+
refactor [label="重构\n清理代码", shape=box, style=filled, fillcolor="#ccccff"];
|
|
57
|
+
next [label="下一个", shape=ellipse];
|
|
58
|
+
|
|
59
|
+
red -> verify_red;
|
|
60
|
+
verify_red -> green [label="是"];
|
|
61
|
+
verify_red -> red [label="错误的\n失败"];
|
|
62
|
+
green -> verify_green;
|
|
63
|
+
verify_green -> refactor [label="是"];
|
|
64
|
+
verify_green -> green [label="否"];
|
|
65
|
+
refactor -> verify_green [label="保持\n绿灯"];
|
|
66
|
+
verify_green -> next;
|
|
67
|
+
next -> red;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 红灯 - 写失败测试
|
|
72
|
+
|
|
73
|
+
写一个最小的测试,展示期望的行为。
|
|
74
|
+
|
|
75
|
+
<Good>
|
|
76
|
+
```typescript
|
|
77
|
+
test('失败操作重试 3 次', async () => {
|
|
78
|
+
let attempts = 0;
|
|
79
|
+
const operation = () => {
|
|
80
|
+
attempts++;
|
|
81
|
+
if (attempts < 3) throw new Error('fail');
|
|
82
|
+
return 'success';
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const result = await retryOperation(operation);
|
|
86
|
+
|
|
87
|
+
expect(result).toBe('success');
|
|
88
|
+
expect(attempts).toBe(3);
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
名称清晰,测试真实行为,只测一件事
|
|
92
|
+
</Good>
|
|
93
|
+
|
|
94
|
+
<Bad>
|
|
95
|
+
```typescript
|
|
96
|
+
test('重试能用', async () => {
|
|
97
|
+
const mock = jest.fn()
|
|
98
|
+
.mockRejectedValueOnce(new Error())
|
|
99
|
+
.mockRejectedValueOnce(new Error())
|
|
100
|
+
.mockResolvedValueOnce('success');
|
|
101
|
+
await retryOperation(mock);
|
|
102
|
+
expect(mock).toHaveBeenCalledTimes(3);
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
名称模糊,测试的是 mock 而不是代码
|
|
106
|
+
</Bad>
|
|
107
|
+
|
|
108
|
+
**要求:**
|
|
109
|
+
- 一个行为
|
|
110
|
+
- 清晰的名称
|
|
111
|
+
- 真实代码(除非不得已才用 mock)
|
|
112
|
+
|
|
113
|
+
### 验证红灯 - 看它失败
|
|
114
|
+
|
|
115
|
+
**必须执行。绝不跳过。**
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm test path/to/test.test.ts
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
确认:
|
|
122
|
+
- 测试失败(不是报错)
|
|
123
|
+
- 失败信息符合预期
|
|
124
|
+
- 因为功能缺失而失败(不是拼写错误)
|
|
125
|
+
|
|
126
|
+
**测试通过了?** 你在测试已有行为。修改测试。
|
|
127
|
+
|
|
128
|
+
**测试报错了?** 修复错误,重新运行直到它正确失败。
|
|
129
|
+
|
|
130
|
+
### 绿灯 - 最少代码
|
|
131
|
+
|
|
132
|
+
写最简单的代码让测试通过。
|
|
133
|
+
|
|
134
|
+
<Good>
|
|
135
|
+
```typescript
|
|
136
|
+
async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
|
|
137
|
+
for (let i = 0; i < 3; i++) {
|
|
138
|
+
try {
|
|
139
|
+
return await fn();
|
|
140
|
+
} catch (e) {
|
|
141
|
+
if (i === 2) throw e;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw new Error('unreachable');
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
刚好够通过
|
|
148
|
+
</Good>
|
|
149
|
+
|
|
150
|
+
<Bad>
|
|
151
|
+
```typescript
|
|
152
|
+
async function retryOperation<T>(
|
|
153
|
+
fn: () => Promise<T>,
|
|
154
|
+
options?: {
|
|
155
|
+
maxRetries?: number;
|
|
156
|
+
backoff?: 'linear' | 'exponential';
|
|
157
|
+
onRetry?: (attempt: number) => void;
|
|
158
|
+
}
|
|
159
|
+
): Promise<T> {
|
|
160
|
+
// YAGNI(你不会需要它的)
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
过度设计
|
|
164
|
+
</Bad>
|
|
165
|
+
|
|
166
|
+
不要添加功能,不要重构其他代码,不要"改进"测试之外的东西。
|
|
167
|
+
|
|
168
|
+
### 验证绿灯 - 看它通过
|
|
169
|
+
|
|
170
|
+
**必须执行。**
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npm test path/to/test.test.ts
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
确认:
|
|
177
|
+
- 测试通过
|
|
178
|
+
- 其他测试仍然通过
|
|
179
|
+
- 输出干净(没有错误、警告)
|
|
180
|
+
|
|
181
|
+
**测试失败了?** 修改代码,不是测试。
|
|
182
|
+
|
|
183
|
+
**其他测试失败了?** 立刻修复。
|
|
184
|
+
|
|
185
|
+
### 重构 - 清理代码
|
|
186
|
+
|
|
187
|
+
只在绿灯之后:
|
|
188
|
+
- 消除重复
|
|
189
|
+
- 改善命名
|
|
190
|
+
- 提取辅助函数
|
|
191
|
+
|
|
192
|
+
保持测试绿灯。不要添加行为。
|
|
193
|
+
|
|
194
|
+
### 循环
|
|
195
|
+
|
|
196
|
+
下一个失败测试,下一个功能。
|
|
197
|
+
|
|
198
|
+
## 好的测试
|
|
199
|
+
|
|
200
|
+
| 质量 | 好 | 坏 |
|
|
201
|
+
|------|------|------|
|
|
202
|
+
| **最小化** | 只测一件事。名称里有"和"?拆分它。 | `test('验证邮箱和域名和空格')` |
|
|
203
|
+
| **清晰** | 名称描述行为 | `test('test1')` |
|
|
204
|
+
| **展示意图** | 展示期望的 API | 模糊了代码应该做什么 |
|
|
205
|
+
|
|
206
|
+
## 为什么顺序很重要
|
|
207
|
+
|
|
208
|
+
**"我先写代码,之后再写测试来验证"**
|
|
209
|
+
|
|
210
|
+
先写代码再写的测试会立刻通过。立刻通过什么都证明不了:
|
|
211
|
+
- 可能测错了东西
|
|
212
|
+
- 可能测的是实现,不是行为
|
|
213
|
+
- 可能遗漏了你忘记的边界情况
|
|
214
|
+
- 你从没看到它捕获过 bug
|
|
215
|
+
|
|
216
|
+
先写测试迫使你看到测试失败,证明它确实在测试某些东西。
|
|
217
|
+
|
|
218
|
+
**"我已经手动测试了所有边界情况"**
|
|
219
|
+
|
|
220
|
+
手动测试是随意的。你以为测了所有情况,但是:
|
|
221
|
+
- 没有记录你测了什么
|
|
222
|
+
- 代码变更后无法重新运行
|
|
223
|
+
- 压力下容易遗漏用例
|
|
224
|
+
- "我试过能用" ≠ 全面覆盖
|
|
225
|
+
|
|
226
|
+
自动化测试是系统性的。每次运行方式完全一样。
|
|
227
|
+
|
|
228
|
+
**"删掉 X 小时的工作太浪费了"**
|
|
229
|
+
|
|
230
|
+
沉没成本谬误。时间已经花了。你现在的选择是:
|
|
231
|
+
- 删掉,用 TDD 重写(再花 X 小时,高置信度)
|
|
232
|
+
- 保留,之后补测试(30 分钟,低置信度,很可能有 bug)
|
|
233
|
+
|
|
234
|
+
"浪费"的是保留你无法信任的代码。没有真正测试的可运行代码就是技术债。
|
|
235
|
+
|
|
236
|
+
**"TDD 太教条了,务实意味着灵活变通"**
|
|
237
|
+
|
|
238
|
+
TDD 就是务实的:
|
|
239
|
+
- 提交前发现 bug(比提交后调试快)
|
|
240
|
+
- 防止回归(测试立刻捕获破坏)
|
|
241
|
+
- 记录行为(测试展示如何使用代码)
|
|
242
|
+
- 支持重构(放心改,测试捕获破坏)
|
|
243
|
+
|
|
244
|
+
"务实"的捷径 = 在生产环境调试 = 更慢。
|
|
245
|
+
|
|
246
|
+
**"后写测试也能达到同样目的,重要的是精神不是仪式"**
|
|
247
|
+
|
|
248
|
+
不对。后写测试回答的是"这段代码做了什么?"先写测试回答的是"这段代码应该做什么?"
|
|
249
|
+
|
|
250
|
+
后写测试受你的实现偏见影响。你测的是你写了什么,不是需求是什么。你验证的是你记得的边界情况,不是你发现的边界情况。
|
|
251
|
+
|
|
252
|
+
先写测试迫使你在实现之前发现边界情况。后写测试验证的是你记住了所有情况(你没有)。
|
|
253
|
+
|
|
254
|
+
30 分钟的后补测试 ≠ TDD。你得到了覆盖率,失去了测试有效的证明。
|
|
255
|
+
|
|
256
|
+
## 常见的自我合理化
|
|
257
|
+
|
|
258
|
+
| 借口 | 现实 |
|
|
259
|
+
|------|------|
|
|
260
|
+
| "太简单不用测" | 简单代码也会出错。测试只要 30 秒。 |
|
|
261
|
+
| "我之后再测" | 立刻通过的测试什么都证明不了。 |
|
|
262
|
+
| "后写测试也能达到同样目的" | 后写测试 = "这做了什么?" 先写测试 = "这应该做什么?" |
|
|
263
|
+
| "已经手动测过了" | 随意 ≠ 系统性。没有记录,无法重跑。 |
|
|
264
|
+
| "删掉 X 小时的工作太浪费" | 沉没成本谬误。保留未验证代码才是技术债。 |
|
|
265
|
+
| "留着当参考,先写测试" | 你会改编它。那就是后写测试。删除就是删除。 |
|
|
266
|
+
| "需要先探索一下" | 可以。扔掉探索结果,从 TDD 开始。 |
|
|
267
|
+
| "测试难写 = 设计不清楚" | 听测试的话。难测试 = 难使用。 |
|
|
268
|
+
| "TDD 会拖慢我" | TDD 比调试快。务实 = 先写测试。 |
|
|
269
|
+
| "手动测试更快" | 手动测试不能证明边界情况。每次改动你都得重测。 |
|
|
270
|
+
| "现有代码没有测试" | 你在改进它。给现有代码加测试。 |
|
|
271
|
+
|
|
272
|
+
## 危险信号 - 停下来,从头开始
|
|
273
|
+
|
|
274
|
+
- 先写了代码再写测试
|
|
275
|
+
- 实现之后才写测试
|
|
276
|
+
- 测试立刻通过
|
|
277
|
+
- 无法解释测试为什么失败
|
|
278
|
+
- 测试"之后再加"
|
|
279
|
+
- 合理化"就这一次"
|
|
280
|
+
- "我已经手动测过了"
|
|
281
|
+
- "后写测试也能达到同样目的"
|
|
282
|
+
- "重要的是精神不是仪式"
|
|
283
|
+
- "留着当参考"或"改编现有代码"
|
|
284
|
+
- "已经花了 X 小时,删掉太浪费"
|
|
285
|
+
- "TDD 太教条了,我是在务实"
|
|
286
|
+
- "这次情况不一样因为..."
|
|
287
|
+
|
|
288
|
+
**以上所有都意味着:删掉代码。用 TDD 从头开始。**
|
|
289
|
+
|
|
290
|
+
## 示例:Bug 修复
|
|
291
|
+
|
|
292
|
+
**Bug:** 空邮箱被接受了
|
|
293
|
+
|
|
294
|
+
**红灯**
|
|
295
|
+
```typescript
|
|
296
|
+
test('拒绝空邮箱', async () => {
|
|
297
|
+
const result = await submitForm({ email: '' });
|
|
298
|
+
expect(result.error).toBe('Email required');
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**验证红灯**
|
|
303
|
+
```bash
|
|
304
|
+
$ npm test
|
|
305
|
+
FAIL: expected 'Email required', got undefined
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**绿灯**
|
|
309
|
+
```typescript
|
|
310
|
+
function submitForm(data: FormData) {
|
|
311
|
+
if (!data.email?.trim()) {
|
|
312
|
+
return { error: 'Email required' };
|
|
313
|
+
}
|
|
314
|
+
// ...
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**验证绿灯**
|
|
319
|
+
```bash
|
|
320
|
+
$ npm test
|
|
321
|
+
PASS
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**重构**
|
|
325
|
+
如果需要,提取验证逻辑用于多个字段。
|
|
326
|
+
|
|
327
|
+
## 验证检查清单
|
|
328
|
+
|
|
329
|
+
标记工作完成之前:
|
|
330
|
+
|
|
331
|
+
- [ ] 每个新函数/方法都有测试
|
|
332
|
+
- [ ] 在实现之前看到每个测试失败
|
|
333
|
+
- [ ] 每个测试因预期原因失败(功能缺失,不是拼写错误)
|
|
334
|
+
- [ ] 为每个测试写了最少的代码让它通过
|
|
335
|
+
- [ ] 所有测试通过
|
|
336
|
+
- [ ] 输出干净(没有错误、警告)
|
|
337
|
+
- [ ] 测试使用真实代码(只在不得已时用 mock)
|
|
338
|
+
- [ ] 覆盖了边界情况和错误情况
|
|
339
|
+
|
|
340
|
+
不能全部勾选?你跳过了 TDD。从头来过。
|
|
341
|
+
|
|
342
|
+
## 验收集成
|
|
343
|
+
|
|
344
|
+
TDD 不只在实现阶段使用。在 specops 验收阶段,TDD 原则同样适用:
|
|
345
|
+
|
|
346
|
+
### 单元测试验收
|
|
347
|
+
- 运行 `npx vitest run` 确保所有单元测试通过
|
|
348
|
+
- 检查测试覆盖率是否满足项目要求
|
|
349
|
+
- 使用 specops 偷懒检测器扫描:不允许 `expect(true).toBe(true)` 等无意义断言
|
|
350
|
+
|
|
351
|
+
### 集成测试验收
|
|
352
|
+
- 运行 `tsc --noEmit` 确保类型安全
|
|
353
|
+
- 运行技术偏移检测(tech-guard)确保未引入未批准依赖
|
|
354
|
+
- 检查是否存在 `as any`、`@ts-ignore` 等禁止模式
|
|
355
|
+
|
|
356
|
+
### E2E 测试验收
|
|
357
|
+
- 由专门的验收 Agent 执行(配置浏览器 MCP)
|
|
358
|
+
- 使用 browser-use `--browser real` 模式或 Playwright MCP
|
|
359
|
+
- 覆盖所有核心业务流程的完整路径
|
|
360
|
+
- 每个业务场景必须有对应的 E2E 用例
|
|
361
|
+
|
|
362
|
+
### 验收运行器
|
|
363
|
+
- specops 验收运行器(`src/acceptance/runner.ts`)串联所有检查
|
|
364
|
+
- 生成 ACCEPTANCE.md 报告
|
|
365
|
+
- 偷懒检测器(lazyDetector)扫描测试文件质量
|
|
366
|
+
- 技术守护(tech-guard)检测技术偏移
|
|
367
|
+
|
|
368
|
+
**验收失败 = 实现未完成。** 必须修复后重新验收。
|
|
369
|
+
|
|
370
|
+
## 卡住时
|
|
371
|
+
|
|
372
|
+
| 问题 | 解决方案 |
|
|
373
|
+
|------|----------|
|
|
374
|
+
| 不知道怎么测 | 写出你期望的 API。先写断言。问你的人类搭档。 |
|
|
375
|
+
| 测试太复杂 | 设计太复杂。简化接口。 |
|
|
376
|
+
| 必须 mock 所有东西 | 代码耦合太紧。使用依赖注入。 |
|
|
377
|
+
| 测试准备工作太多 | 提取辅助函数。还是复杂?简化设计。 |
|
|
378
|
+
|
|
379
|
+
## 调试集成
|
|
380
|
+
|
|
381
|
+
发现 bug?写一个失败测试来复现它。遵循 TDD 循环。测试证明修复有效并防止回归。
|
|
382
|
+
|
|
383
|
+
永远不要在没有测试的情况下修 bug。
|
|
384
|
+
|
|
385
|
+
## 测试反模式
|
|
386
|
+
|
|
387
|
+
添加 mock 或测试工具时,阅读 @testing-anti-patterns.md 避免常见陷阱:
|
|
388
|
+
- 测试 mock 行为而不是真实行为
|
|
389
|
+
- 给生产类添加仅测试用的方法
|
|
390
|
+
- 不理解依赖关系就使用 mock
|
|
391
|
+
|
|
392
|
+
## 最终规则
|
|
393
|
+
|
|
394
|
+
```
|
|
395
|
+
生产代码 → 测试存在且先失败过
|
|
396
|
+
否则 → 不是 TDD
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
没有你的人类搭档的许可,没有例外。
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# 测试反模式
|
|
2
|
+
|
|
3
|
+
**在以下情况加载此参考文档:** 编写或修改测试、添加 mock、或想给生产代码添加仅测试用方法时。
|
|
4
|
+
|
|
5
|
+
## 概述
|
|
6
|
+
|
|
7
|
+
测试必须验证真实行为,不是 mock 行为。Mock 是隔离的手段,不是被测试的对象。
|
|
8
|
+
|
|
9
|
+
**核心原则:** 测试代码做了什么,不是 mock 做了什么。
|
|
10
|
+
|
|
11
|
+
**严格遵循 TDD 可以防止这些反模式。**
|
|
12
|
+
|
|
13
|
+
## 铁律
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
1. 绝不测试 mock 行为
|
|
17
|
+
2. 绝不给生产类添加仅测试用的方法
|
|
18
|
+
3. 绝不在不理解依赖关系的情况下使用 mock
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 反模式 1:测试 Mock 行为
|
|
22
|
+
|
|
23
|
+
**违规做法:**
|
|
24
|
+
```typescript
|
|
25
|
+
// ❌ 坏:测试 mock 是否存在
|
|
26
|
+
test('渲染侧边栏', () => {
|
|
27
|
+
render(<Page />);
|
|
28
|
+
expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**为什么这是错的:**
|
|
33
|
+
- 你在验证 mock 能用,不是组件能用
|
|
34
|
+
- mock 存在就通过,不存在就失败
|
|
35
|
+
- 对真实行为一无所知
|
|
36
|
+
|
|
37
|
+
**你的人类搭档的纠正:** "我们是在测试 mock 的行为吗?"
|
|
38
|
+
|
|
39
|
+
**正确做法:**
|
|
40
|
+
```typescript
|
|
41
|
+
// ✅ 好:测试真实组件或不要 mock 它
|
|
42
|
+
test('渲染侧边栏', () => {
|
|
43
|
+
render(<Page />); // 不要 mock 侧边栏
|
|
44
|
+
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// 或者如果必须 mock 侧边栏来隔离:
|
|
48
|
+
// 不要断言 mock 本身,测试 Page 在侧边栏存在时的行为
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 门控函数
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
在断言任何 mock 元素之前:
|
|
55
|
+
问自己:"我是在测试真实组件行为还是只是 mock 的存在?"
|
|
56
|
+
|
|
57
|
+
如果是测试 mock 的存在:
|
|
58
|
+
停下来 - 删除断言或取消 mock
|
|
59
|
+
|
|
60
|
+
测试真实行为
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 反模式 2:给生产代码添加仅测试用的方法
|
|
64
|
+
|
|
65
|
+
**违规做法:**
|
|
66
|
+
```typescript
|
|
67
|
+
// ❌ 坏:destroy() 只在测试中使用
|
|
68
|
+
class Session {
|
|
69
|
+
async destroy() { // 看起来像生产 API!
|
|
70
|
+
await this._workspaceManager?.destroyWorkspace(this.id);
|
|
71
|
+
// ... 清理
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 在测试中
|
|
76
|
+
afterEach(() => session.destroy());
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**为什么这是错的:**
|
|
80
|
+
- 生产类被仅测试用的代码污染
|
|
81
|
+
- 如果在生产环境中意外调用很危险
|
|
82
|
+
- 违反 YAGNI 和关注点分离
|
|
83
|
+
- 混淆了对象生命周期和实体生命周期
|
|
84
|
+
|
|
85
|
+
**正确做法:**
|
|
86
|
+
```typescript
|
|
87
|
+
// ✅ 好:测试工具处理测试清理
|
|
88
|
+
// Session 没有 destroy() - 它在生产环境中是无状态的
|
|
89
|
+
|
|
90
|
+
// 在 test-utils/ 中
|
|
91
|
+
export async function cleanupSession(session: Session) {
|
|
92
|
+
const workspace = session.getWorkspaceInfo();
|
|
93
|
+
if (workspace) {
|
|
94
|
+
await workspaceManager.destroyWorkspace(workspace.id);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 在测试中
|
|
99
|
+
afterEach(() => cleanupSession(session));
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 门控函数
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
在给生产类添加任何方法之前:
|
|
106
|
+
问自己:"这个方法只在测试中使用吗?"
|
|
107
|
+
|
|
108
|
+
如果是:
|
|
109
|
+
停下来 - 不要添加
|
|
110
|
+
放到测试工具中
|
|
111
|
+
|
|
112
|
+
问自己:"这个类拥有这个资源的生命周期吗?"
|
|
113
|
+
|
|
114
|
+
如果不是:
|
|
115
|
+
停下来 - 这个方法放错了类
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## 反模式 3:不理解依赖就使用 Mock
|
|
119
|
+
|
|
120
|
+
**违规做法:**
|
|
121
|
+
```typescript
|
|
122
|
+
// ❌ 坏:Mock 破坏了测试逻辑
|
|
123
|
+
test('检测重复服务器', () => {
|
|
124
|
+
// Mock 阻止了测试依赖的配置写入!
|
|
125
|
+
vi.mock('ToolCatalog', () => ({
|
|
126
|
+
discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
await addServer(config);
|
|
130
|
+
await addServer(config); // 应该抛异常,但不会!
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**为什么这是错的:**
|
|
135
|
+
- 被 mock 的方法有测试依赖的副作用(写入配置)
|
|
136
|
+
- 为了"安全"过度 mock 破坏了实际行为
|
|
137
|
+
- 测试因为错误的原因通过或莫名其妙地失败
|
|
138
|
+
|
|
139
|
+
**正确做法:**
|
|
140
|
+
```typescript
|
|
141
|
+
// ✅ 好:在正确的层级 mock
|
|
142
|
+
test('检测重复服务器', () => {
|
|
143
|
+
// Mock 慢的部分,保留测试需要的行为
|
|
144
|
+
vi.mock('MCPServerManager'); // 只 mock 慢的服务器启动
|
|
145
|
+
|
|
146
|
+
await addServer(config); // 配置被写入
|
|
147
|
+
await addServer(config); // 检测到重复 ✓
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 门控函数
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
在 mock 任何方法之前:
|
|
155
|
+
停下来 - 先不要 mock
|
|
156
|
+
|
|
157
|
+
1. 问自己:"真实方法有什么副作用?"
|
|
158
|
+
2. 问自己:"这个测试依赖这些副作用吗?"
|
|
159
|
+
3. 问自己:"我完全理解这个测试需要什么吗?"
|
|
160
|
+
|
|
161
|
+
如果依赖副作用:
|
|
162
|
+
在更低层级 mock(实际慢的/外部的操作)
|
|
163
|
+
或者使用保留必要行为的测试替身
|
|
164
|
+
不要 mock 测试依赖的高层方法
|
|
165
|
+
|
|
166
|
+
如果不确定测试依赖什么:
|
|
167
|
+
先用真实实现运行测试
|
|
168
|
+
观察实际需要发生什么
|
|
169
|
+
然后在正确的层级添加最少的 mock
|
|
170
|
+
|
|
171
|
+
危险信号:
|
|
172
|
+
- "我 mock 这个以防万一"
|
|
173
|
+
- "这个可能很慢,最好 mock 掉"
|
|
174
|
+
- 不理解依赖链就 mock
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## 反模式 4:不完整的 Mock
|
|
178
|
+
|
|
179
|
+
**违规做法:**
|
|
180
|
+
```typescript
|
|
181
|
+
// ❌ 坏:部分 mock - 只有你认为需要的字段
|
|
182
|
+
const mockResponse = {
|
|
183
|
+
status: 'success',
|
|
184
|
+
data: { userId: '123', name: 'Alice' }
|
|
185
|
+
// 缺少:下游代码使用的 metadata
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// 之后:代码访问 response.metadata.requestId 时崩溃
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**为什么这是错的:**
|
|
192
|
+
- **部分 mock 隐藏了结构假设** - 你只 mock 了你知道的字段
|
|
193
|
+
- **下游代码可能依赖你没包含的字段** - 静默失败
|
|
194
|
+
- **测试通过但集成失败** - Mock 不完整,真实 API 完整
|
|
195
|
+
- **虚假的信心** - 测试对真实行为什么都没证明
|
|
196
|
+
|
|
197
|
+
**铁律:** Mock 完整的数据结构,按照它在现实中的样子,不只是你当前测试用到的字段。
|
|
198
|
+
|
|
199
|
+
**正确做法:**
|
|
200
|
+
```typescript
|
|
201
|
+
// ✅ 好:镜像真实 API 的完整性
|
|
202
|
+
const mockResponse = {
|
|
203
|
+
status: 'success',
|
|
204
|
+
data: { userId: '123', name: 'Alice' },
|
|
205
|
+
metadata: { requestId: 'req-789', timestamp: 1234567890 }
|
|
206
|
+
// 真实 API 返回的所有字段
|
|
207
|
+
};
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 门控函数
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
在创建 mock 响应之前:
|
|
214
|
+
检查:"真实 API 响应包含哪些字段?"
|
|
215
|
+
|
|
216
|
+
操作:
|
|
217
|
+
1. 从文档/示例中检查实际 API 响应
|
|
218
|
+
2. 包含系统下游可能消费的所有字段
|
|
219
|
+
3. 验证 mock 完全匹配真实响应的 schema
|
|
220
|
+
|
|
221
|
+
关键:
|
|
222
|
+
如果你在创建 mock,你必须理解完整的结构
|
|
223
|
+
部分 mock 在代码依赖被省略字段时静默失败
|
|
224
|
+
|
|
225
|
+
不确定时:包含所有文档记录的字段
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## 反模式 5:集成测试当事后补充
|
|
229
|
+
|
|
230
|
+
**违规做法:**
|
|
231
|
+
```
|
|
232
|
+
✅ 实现完成
|
|
233
|
+
❌ 没写测试
|
|
234
|
+
"准备好测试了"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**为什么这是错的:**
|
|
238
|
+
- 测试是实现的一部分,不是可选的后续步骤
|
|
239
|
+
- TDD 本来就能避免这个问题
|
|
240
|
+
- 没有测试不能声称完成
|
|
241
|
+
|
|
242
|
+
**正确做法:**
|
|
243
|
+
```
|
|
244
|
+
TDD 循环:
|
|
245
|
+
1. 写失败测试
|
|
246
|
+
2. 实现让它通过
|
|
247
|
+
3. 重构
|
|
248
|
+
4. 然后才能声称完成
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## 当 Mock 变得太复杂
|
|
252
|
+
|
|
253
|
+
**警告信号:**
|
|
254
|
+
- Mock 准备工作比测试逻辑还长
|
|
255
|
+
- 为了让测试通过 mock 了所有东西
|
|
256
|
+
- Mock 缺少真实组件有的方法
|
|
257
|
+
- 改了 mock 测试就挂
|
|
258
|
+
|
|
259
|
+
**你的人类搭档的问题:** "我们这里真的需要用 mock 吗?"
|
|
260
|
+
|
|
261
|
+
**考虑:** 使用真实组件的集成测试通常比复杂的 mock 更简单
|
|
262
|
+
|
|
263
|
+
## TDD 如何防止这些反模式
|
|
264
|
+
|
|
265
|
+
**为什么 TDD 有帮助:**
|
|
266
|
+
1. **先写测试** → 迫使你思考你到底在测什么
|
|
267
|
+
2. **看它失败** → 确认测试测的是真实行为,不是 mock
|
|
268
|
+
3. **最少实现** → 不会悄悄加入仅测试用的方法
|
|
269
|
+
4. **真实依赖** → 你在 mock 之前就看到测试实际需要什么
|
|
270
|
+
|
|
271
|
+
**如果你在测试 mock 行为,你违反了 TDD** - 你在没有先让测试对真实代码失败的情况下就加了 mock。
|
|
272
|
+
|
|
273
|
+
## 快速参考
|
|
274
|
+
|
|
275
|
+
| 反模式 | 修复方法 |
|
|
276
|
+
|--------|----------|
|
|
277
|
+
| 断言 mock 元素 | 测试真实组件或取消 mock |
|
|
278
|
+
| 生产代码中的仅测试方法 | 移到测试工具中 |
|
|
279
|
+
| 不理解就 mock | 先理解依赖关系,最少化 mock |
|
|
280
|
+
| 不完整的 mock | 完整镜像真实 API |
|
|
281
|
+
| 测试当事后补充 | TDD - 先写测试 |
|
|
282
|
+
| 过于复杂的 mock | 考虑集成测试 |
|
|
283
|
+
|
|
284
|
+
## 危险信号
|
|
285
|
+
|
|
286
|
+
- 断言检查 `*-mock` 测试 ID
|
|
287
|
+
- 方法只在测试文件中被调用
|
|
288
|
+
- Mock 准备工作占测试的 >50%
|
|
289
|
+
- 移除 mock 测试就失败
|
|
290
|
+
- 无法解释为什么需要 mock
|
|
291
|
+
- "以防万一"就 mock
|
|
292
|
+
|
|
293
|
+
## 底线
|
|
294
|
+
|
|
295
|
+
**Mock 是隔离的工具,不是被测试的对象。**
|
|
296
|
+
|
|
297
|
+
如果 TDD 揭示你在测试 mock 行为,你走错了方向。
|
|
298
|
+
|
|
299
|
+
修复方法:测试真实行为,或者质疑你为什么要 mock。
|