superpowers-zh 1.7.4 → 1.7.5
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/README.zh-Hant.md +3 -1
- package/RELEASE-NOTES.zh.md +51 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/skills/finishing-a-development-branch/SKILL.md +8 -9
- package/skills/test-driven-development/SKILL.md +10 -61
- package/skills/test-driven-development/writing-good-tests.md +145 -0
- package/skills/using-superpowers/references/gemini-tools.md +55 -25
- package/skills/test-driven-development/testing-anti-patterns.md +0 -299
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "superpowers-zh",
|
|
11
11
|
"description": "AI 编程超能力中文增强版:20 个 skills(14 翻译 + 4 中国原创 + 2 上游历史保留),支持 Claude Code / Hermes Agent / Cursor / Claw Code / Qoder 等 22 款工具",
|
|
12
|
-
"version": "1.7.
|
|
12
|
+
"version": "1.7.5",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "jnMetaCode",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superpowers-zh",
|
|
3
3
|
"description": "AI 编程超能力中文增强版:20 个 skills(14 翻译 + 4 中国原创 + 2 上游历史保留),支持 Claude Code / Hermes Agent / Cursor / Claw Code / Qoder 等 22 款工具",
|
|
4
|
-
"version": "1.7.
|
|
4
|
+
"version": "1.7.5",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "jnMetaCode",
|
|
7
7
|
"url": "https://github.com/jnMetaCode"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "superpowers-zh",
|
|
3
3
|
"displayName": "Superpowers 中文版",
|
|
4
4
|
"description": "AI 编程超能力中文增强版:20 个 skills(14 翻译 + 4 中国原创 + 2 上游历史保留),支持 Cursor / Claude Code / Hermes Agent / Claw Code / Qoder 等 22 款工具",
|
|
5
|
-
"version": "1.7.
|
|
5
|
+
"version": "1.7.5",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "jnMetaCode",
|
|
8
8
|
"url": "https://github.com/jnMetaCode"
|
package/README.md
CHANGED
|
@@ -16,7 +16,9 @@ Chinese community edition of [superpowers](https://github.com/obra/superpowers)
|
|
|
16
16
|
>
|
|
17
17
|
> 🌍 Also available in [English](https://aiolaola.com/en?utm_source=github&utm_campaign=superpowers) · [日本語](https://aiolaola.com/ja?utm_source=github&utm_campaign=superpowers) · [Español](https://aiolaola.com/es?utm_source=github&utm_campaign=superpowers) · [한국어](https://aiolaola.com/ko?utm_source=github&utm_campaign=superpowers) · [繁體中文](https://aiolaola.com/zh-Hant?utm_source=github&utm_campaign=superpowers)
|
|
18
18
|
|
|
19
|
-
> 🆕 **v1.7.
|
|
19
|
+
> 🆕 **v1.7.5 更新亮点**([完整 Release Notes →](RELEASE-NOTES.zh.md))
|
|
20
|
+
> - 🐛 **两个 worktree / Gemini 的真问题** —— 修掉 worktree 清理静默空转;更正「Gemini 不支持子智能体」的错误说法(原说法会让 3 个 skill 在 Gemini CLI 上瘸腿)
|
|
21
|
+
> - 🔄 **测试参考重构** —— `testing-anti-patterns` → `writing-good-tests`:从 5 个反模式清单改为两条原则 + 变异检查
|
|
20
22
|
> - 🔄 **SDD 同步上游 v6.2.0** —— plan 作用域工作区(一份过期账本再也不会让控制者跳过整段任务)+ 五轮上限的唤回式修复循环与熔断裁定
|
|
21
23
|
> - 🪟 **Windows bootstrap 修复** —— SessionStart hook 改经 Git Bash 分发(同步上游),hook 不加载 skill 就是死重
|
|
22
24
|
> - 🧩 新增 **Cline** 与 **Kilo Code** 两款 VS Code 扩展(工具数 20 → 22)—— 按 rules 常驻开销做了专门设计,索引仅 4.5 KB
|
package/README.zh-Hant.md
CHANGED
|
@@ -16,7 +16,9 @@ Chinese community edition of [superpowers](https://github.com/obra/superpowers)
|
|
|
16
16
|
|
|
17
17
|
> 📖 **免費配套學習** → [從零學會 AI 編程](https://aiolaola.com/?utm_source=github&utm_campaign=superpowers):180 節免費實操課 + 《AI 編程實戰三卷書》線上閱讀 + 實戰社群 · superpowers 裝好後配上方法論效率翻倍 · 永久免費
|
|
18
18
|
|
|
19
|
-
> 🆕 **v1.7.
|
|
19
|
+
> 🆕 **v1.7.5 更新亮點**([完整 Release Notes →](RELEASE-NOTES.zh.md))
|
|
20
|
+
> - 🐛 **兩個 worktree / Gemini 的真問題** —— 修掉 worktree 清理靜默空轉;更正「Gemini 不支援子智能體」的錯誤說法(原說法會讓 3 個 skill 在 Gemini CLI 上瘸腿)
|
|
21
|
+
> - 🔄 **測試參考重構** —— `testing-anti-patterns` → `writing-good-tests`:從 5 個反模式清單改為兩條原則 + 變異檢查
|
|
20
22
|
> - 🔄 **SDD 同步上游 v6.2.0** —— plan 作用域工作區(一份過期帳本再也不會讓控制者跳過整段任務)+ 五輪上限的喚回式修復循環與熔斷裁定
|
|
21
23
|
> - 🪟 **Windows bootstrap 修復** —— SessionStart hook 改經 Git Bash 分發(同步上游),hook 不載入 skill 就是死重
|
|
22
24
|
> - 🧩 新增 **Cline** 與 **Kilo Code** 兩款 VS Code 外掛(工具數 20 → 22)—— 針對 rules 常駐開銷做了專門設計,索引僅 4.5 KB
|
package/RELEASE-NOTES.zh.md
CHANGED
|
@@ -6,6 +6,57 @@
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## v1.7.5 (2026-08-07)
|
|
10
|
+
|
|
11
|
+
对齐上游 v6.2.0 的 **B 块 + D 块**([#19](https://github.com/jnMetaCode/superpowers-zh/issues/19))。
|
|
12
|
+
|
|
13
|
+
### 🐛 worktree 清理静默空转(真 bug,我们与上游同样存在)
|
|
14
|
+
|
|
15
|
+
`finishing-a-development-branch` 的步骤 6 在步骤 5 已经 `cd` 到主仓库根之后,才用 `git rev-parse --show-toplevel` 重算 `WORKTREE_PATH` —— 于是拿到主根路径,`.worktrees/` 溯源判断**永远匹配不上**,清理静默空转,随后分支删除还会因为 worktree 仍挂着而失败。上游记录说测试对象不得不偏离 skill 原文才能跑通。
|
|
16
|
+
|
|
17
|
+
修法:步骤 2 趁还在工作区内就捕获,步骤 6 消费该值并加显式警告说明为何不能重算;去掉步骤 6 冗余的 `MAIN_ROOT` 推导与 `cd`;选项 2 补上菜单已声称的分离 HEAD 推送变体。
|
|
18
|
+
|
|
19
|
+
**已实际复现验证**(临时仓库 + `.worktrees/feature`):旧逻辑算得 `main` → 溯源未命中 → 清理空转;新逻辑捕获到 `main/.worktrees/feature` → 命中 → `worktree remove` 与 `branch -D` 均成功。
|
|
20
|
+
|
|
21
|
+
### 🐛 我们把 Gemini 的子智能体支持写错了
|
|
22
|
+
|
|
23
|
+
`gemini-tools.md` 原文称 Gemini CLI 没有 Task 等价物、依赖子智能体的 skill「退化为 `executing-plans` 单会话执行」。**这是错的** —— Gemini CLI 通过 `invoke_agent`(`agent_name: "generalist"`,也可用 `@generalist` 聊天语法)支持子智能体,且支持同一响应内多调用并行分派。
|
|
24
|
+
|
|
25
|
+
这个错误会让 Gemini CLI 用户的 `subagent-driven-development` / `dispatching-parallel-agents` / `requesting-code-review` 全部瘸腿。按上游重写(33 → 62 行),补齐指令文件层级加载、`~/.gemini/skills` 与 `~/.agents/skills` 优先级、模板填写、并行分派,以及此前缺失的 20 个工具名。已全仓扫描确认无别处重复该说法。
|
|
26
|
+
|
|
27
|
+
### 🔄 `testing-anti-patterns.md` → `writing-good-tests.md`
|
|
28
|
+
|
|
29
|
+
上游把 299 行的反模式枚举重写为 198 行的**两条原则**:
|
|
30
|
+
|
|
31
|
+
- **原则 1「点名它要抓的破坏」** —— 写测试体前先答"什么生产改动会让它失败,那是 bug 还是决定"。含镜像断言、变更探测器、测行为不测文本、测你的代码不测框架
|
|
32
|
+
- **原则 2「跑真东西」** —— mock 不配拥有断言、在正确层级 mock、替身要具体、完整镜像真实数据、生产类只承载生产方法
|
|
33
|
+
- 新增**变异检查**:收尾前在脑中变异生产代码,每种现实变异都应至少让一个测试失败
|
|
34
|
+
- 触发条件放宽到「编写或修改**任何**测试时」
|
|
35
|
+
|
|
36
|
+
TDD SKILL.md 同步:删掉「为什么顺序很重要」整节长散文(论点折进合理化借口表,5 行扩写)、删掉末尾已失效的「测试反模式」一节。
|
|
37
|
+
|
|
38
|
+
### 🔧 audit 结构漂移度量修正(此前一直在虚报欠账)
|
|
39
|
+
|
|
40
|
+
`audit.sh` 用 `grep -cE '^#{1,4} '` 数标题,但这会把 ``` 围栏内的 shell 注释(`# 运行测试`)当成 markdown 标题 —— 多几行 bash 注释就能凭空造出「结构漂移」。
|
|
41
|
+
|
|
42
|
+
用正确口径(awk 逐行跟踪围栏)重算 14 个 skill,3 条告警里 **2 条是假阳性**:
|
|
43
|
+
|
|
44
|
+
| skill | 旧口径 | 新口径 |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `executing-plans` | 9/16 **WARN** | 9/11 pass |
|
|
47
|
+
| `finishing-a-development-branch` | 21/31 **WARN** | 14/17 pass |
|
|
48
|
+
| `using-git-worktrees` | 21/28 WARN | 14/21 **WARN(真漂移)** |
|
|
49
|
+
|
|
50
|
+
双向验证:给 `brainstorming` 加 5 个真标题会触发告警,加 5 行围栏内 shell 注释不触发。
|
|
51
|
+
|
|
52
|
+
### ✅ 验证
|
|
53
|
+
|
|
54
|
+
- 两个新/改文件章节均与上游一一对应;`writing-good-tests.md` 指标精确一致(12 bold 要点 / 11 行表格 / 11 条危险信号 / 14 项代码记号)
|
|
55
|
+
- **行为 eval 8 个判断场景:8/8 全对**。附带一个有价值的对照:agent 拿不到 `writing-good-tests.md`、只能退回 SKILL.md 四条摘要时得分 6/8 —— 错的恰好是只存在于参考文件里的两条规则(部分 mock 静默失败、琐碎转发 getter 不配有测试)。说明参考文件承载着摘要覆盖不到的承重规则。
|
|
56
|
+
- `scripts/audit.sh` **152 pass / 1 warn / 0 fail**(原 150/3)、`scripts/verify-release.sh` **82 pass / 0 fail**
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
9
60
|
## v1.7.4 (2026-08-07)
|
|
10
61
|
|
|
11
62
|
### 🔄 SDD 同步上游 v6.2.0:plan 作用域工作区 + 基于唤回的修复循环(#19 A 块)
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
|
@@ -50,6 +50,9 @@ npm test / cargo test / pytest / go test ./...
|
|
|
50
50
|
```bash
|
|
51
51
|
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
|
52
52
|
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
|
53
|
+
# 现在就捕获 —— 此刻还在工作区里面。步骤 5 会切换目录,
|
|
54
|
+
# 而清理(步骤 6)需要这个值
|
|
55
|
+
WORKTREE_PATH=$(git rev-parse --show-toplevel)
|
|
53
56
|
```
|
|
54
57
|
|
|
55
58
|
这决定了展示哪种菜单、以及清理方式:
|
|
@@ -129,6 +132,8 @@ git branch -d <feature-branch>
|
|
|
129
132
|
```bash
|
|
130
133
|
# 推送分支
|
|
131
134
|
git push -u origin <feature-branch>
|
|
135
|
+
# 从分离 HEAD 出发时,在远端指定新分支名:
|
|
136
|
+
# git push origin HEAD:refs/heads/<new-branch>
|
|
132
137
|
|
|
133
138
|
# 创建 PR
|
|
134
139
|
gh pr create --title "<title>" --body "$(cat <<'EOF'
|
|
@@ -179,21 +184,15 @@ git branch -D <feature-branch>
|
|
|
179
184
|
|
|
180
185
|
### 步骤 6:清理工作区
|
|
181
186
|
|
|
182
|
-
**只对选项 1 和 4 执行。** 选项 2 和 3 始终保留 worktree
|
|
187
|
+
**只对选项 1 和 4 执行。** 选项 2 和 3 始终保留 worktree。两个调用方都已经切到主仓库根目录了 —— 移除 worktree 必须从 worktree 外面执行 —— 因此这里使用**步骤 2 里捕获的** `GIT_DIR` / `GIT_COMMON` / `WORKTREE_PATH`,也就是那次目录切换之前的值。
|
|
183
188
|
|
|
184
|
-
|
|
185
|
-
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
|
186
|
-
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
|
187
|
-
WORKTREE_PATH=$(git rev-parse --show-toplevel)
|
|
188
|
-
```
|
|
189
|
+
> ⚠️ **不要在这里重新计算这些值。** 此刻 `git rev-parse --show-toplevel` 返回的是主仓库根目录,不是 worktree 路径 —— 溯源判断会永远匹配不上,清理会静默空转,随后分支删除还会因为 worktree 仍挂着而失败。
|
|
189
190
|
|
|
190
191
|
**如果 `GIT_DIR == GIT_COMMON`:** 普通仓库,无 worktree 可清理。结束。
|
|
191
192
|
|
|
192
|
-
**如果
|
|
193
|
+
**如果 `WORKTREE_PATH` 在 `.worktrees/` 或 `worktrees/` 之下:** 这是 Superpowers 创建的 worktree —— 我们负责清理。
|
|
193
194
|
|
|
194
195
|
```bash
|
|
195
|
-
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
|
|
196
|
-
cd "$MAIN_ROOT"
|
|
197
196
|
git worktree remove "$WORKTREE_PATH"
|
|
198
197
|
git worktree prune # 自愈:清理任何过期的注册记录
|
|
199
198
|
```
|
|
@@ -208,69 +208,25 @@ npm test path/to/test.test.ts
|
|
|
208
208
|
| **清晰** | 名称描述行为 | `test('test1')` |
|
|
209
209
|
| **展示意图** | 展示期望的 API | 掩盖了代码应该做什么 |
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
- 可能测试了错误的东西
|
|
217
|
-
- 可能测试的是实现而非行为
|
|
218
|
-
- 可能遗漏了你忘掉的边界情况
|
|
219
|
-
- 你从未看到它捕获 bug
|
|
220
|
-
|
|
221
|
-
先写测试迫使你看到测试失败,证明它确实在测试某些东西。
|
|
222
|
-
|
|
223
|
-
**"我已经手动测试了所有边界情况"**
|
|
224
|
-
|
|
225
|
-
手动测试是临时的。你以为你测试了所有情况,但是:
|
|
226
|
-
- 没有测试记录
|
|
227
|
-
- 代码变更后无法重新运行
|
|
228
|
-
- 在压力下容易遗忘
|
|
229
|
-
- "我试过了能跑" 不等于 全面测试
|
|
230
|
-
|
|
231
|
-
自动化测试是系统性的。它们每次以相同方式运行。
|
|
232
|
-
|
|
233
|
-
**"删除 X 小时的工作太浪费了"**
|
|
234
|
-
|
|
235
|
-
沉没成本谬误。时间已经花了。你现在的选择:
|
|
236
|
-
- 删除并用 TDD 重写(再花 X 小时,高信心)
|
|
237
|
-
- 保留并后补测试(30 分钟,低信心,可能有 bug)
|
|
238
|
-
|
|
239
|
-
"浪费"的是保留你无法信任的代码。没有真正测试的可运行代码就是技术债。
|
|
240
|
-
|
|
241
|
-
**"TDD 太教条了,务实意味着灵活变通"**
|
|
242
|
-
|
|
243
|
-
TDD 就是务实的:
|
|
244
|
-
- 在 commit 前发现 bug(比事后调试快)
|
|
245
|
-
- 防止回归(测试立即发现破坏)
|
|
246
|
-
- 记录行为(测试展示如何使用代码)
|
|
247
|
-
- 支持重构(放心修改,测试捕获破坏)
|
|
248
|
-
|
|
249
|
-
"务实的"捷径 = 在生产环境调试 = 更慢。
|
|
250
|
-
|
|
251
|
-
**"后补测试也能达到相同目的——重要的是精神不是仪式"**
|
|
252
|
-
|
|
253
|
-
不对。后补测试回答"这段代码做了什么?"先写测试回答"这段代码应该做什么?"
|
|
254
|
-
|
|
255
|
-
后补测试受你实现的偏见影响。你测试的是你构建的东西,而非需求要求的。你验证的是你记得的边界情况,而非发现的。
|
|
256
|
-
|
|
257
|
-
先写测试迫使你在实现前发现边界情况。后补测试验证的是你记住了所有情况(你没有)。
|
|
258
|
-
|
|
259
|
-
30 分钟的后补测试 ≠ TDD。你得到了覆盖率,但失去了测试有效的证明。
|
|
211
|
+
写任何测试、或修改任何测试时,阅读 [writing-good-tests.md](writing-good-tests.md),那里是让测试保持诚实的规则:
|
|
212
|
+
- 在动手写之前,先点名那个会让该测试失败的生产代码改动
|
|
213
|
+
- 断言真实行为,绝不断言 mock 行为
|
|
214
|
+
- 只有测试才用的代码放在测试工具里,不进生产类
|
|
215
|
+
- 在 mock 一个依赖之前,先搞清它的副作用
|
|
260
216
|
|
|
261
217
|
## 常见借口
|
|
262
218
|
|
|
263
219
|
| 借口 | 现实 |
|
|
264
220
|
|------|------|
|
|
265
221
|
| "太简单了不用测" | 简单的代码也会出 bug。测试只需 30 秒。 |
|
|
266
|
-
| "我之后补测试" |
|
|
267
|
-
| "
|
|
268
|
-
| "已经手动测试过了" |
|
|
269
|
-
| "删除 X 小时的工作太浪费" |
|
|
222
|
+
| "我之后补测试" | 后写的测试立即通过——而立即通过什么都证明不了。它可能测错了对象、测的是实现而不是行为、或者漏掉你忘了的那个边界情况。你从没看着它失败过,所以你从没证明它能抓住 bug。先写测试逼你看到那次失败。 |
|
|
223
|
+
| "后补测试也能达到相同目的(重的是精神不是仪式)" | 后补测试回答的是"这做了什么?";先写测试回答的是"这应该做什么?"后写的测试已经被你写好的代码带偏了——你验证的是你**记得**的那些情况,而不是你本该**发现**的那些。有覆盖率,没有测试有效的证明。 |
|
|
224
|
+
| "已经手动测试过了" | 手动测试是临时的:没有记录你覆盖了什么、代码一改就没法重跑、压力之下极易漏掉情况。"我试的时候是好的" ≠ 全面。自动化测试每次都以同样的方式运行。 |
|
|
225
|
+
| "删除 X 小时的工作太浪费" | 沉没成本谬误——那些时间无论怎样都已经花掉了。真正的选择是:用 TDD 重写(高置信度)vs 留着它事后补测试(低置信度、很可能有 bug)。留着你无法信任的代码才是浪费。 |
|
|
270
226
|
| "留作参考,然后先写测试" | 你会去改编它。那就是后补测试。删除就是删除。 |
|
|
271
227
|
| "需要先探索一下" | 可以。探索完了扔掉,从 TDD 开始。 |
|
|
272
228
|
| "测试难写 = 设计不清楚" | 听测试的。难以测试 = 难以使用。 |
|
|
273
|
-
| "TDD 会拖慢我" | TDD
|
|
229
|
+
| "TDD 会拖慢我" | TDD **就是**务实的那条路:在提交前抓住 bug、防止回归、让你能无所畏惧地重构。所谓"务实"的抄近道,等于在生产环境里调试——更慢,不是更快。 |
|
|
274
230
|
| "手动测试更快" | 手动测试无法证明边界情况。每次修改你都得重新测。 |
|
|
275
231
|
| "现有代码没有测试" | 你在改进它。为现有代码补测试。 |
|
|
276
232
|
|
|
@@ -359,13 +315,6 @@ PASS
|
|
|
359
315
|
|
|
360
316
|
绝不在没有测试的情况下修复 bug。
|
|
361
317
|
|
|
362
|
-
## 测试反模式
|
|
363
|
-
|
|
364
|
-
添加 mock 或测试工具时,阅读 @testing-anti-patterns.md 以避免常见陷阱:
|
|
365
|
-
- 测试 mock 行为而非真实行为
|
|
366
|
-
- 在生产类中添加仅测试用的方法
|
|
367
|
-
- 在不理解依赖的情况下使用 mock
|
|
368
|
-
|
|
369
318
|
## 最终规则
|
|
370
319
|
|
|
371
320
|
```
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# 写好测试
|
|
2
|
+
|
|
3
|
+
**在以下情况加载此参考:** 编写或修改测试、添加 mock、或为测试添加清理/辅助方法时。
|
|
4
|
+
|
|
5
|
+
## 概述
|
|
6
|
+
|
|
7
|
+
一个测试的存在是为了抓住某个**具体的**破坏。这里的一切都由两条原则统辖:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
1. 每个测试都点名它要抓的破坏
|
|
11
|
+
2. 每个测试都跑真东西
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
严格的 TDD 会自然产出这两点:一个先写、并且在真实代码上亲眼看着它失败过的测试,已经证明了自己**能**失败;而只有当真实依赖被证明缓慢或属于外部时,mock 才配被引入。
|
|
15
|
+
|
|
16
|
+
## 原则 1:点名它要抓的破坏
|
|
17
|
+
|
|
18
|
+
在写测试体之前,先回答:**什么样的生产代码改动应该让这个测试失败——而那个改动是 bug 还是一个决定?** 一个测试靠抓住走错的分支、缺失的副作用、传错的参数、边界情况或被破坏的契约来赢得它的位置。
|
|
19
|
+
|
|
20
|
+
**独立推导期望值。** 用字面量和手工核对过的 fixture;带字面量 `want` 值的表驱动测试是首选形态。一个由**被测代码本身**(或它的辅助函数)算出来的期望值,无论那段代码干了什么都会通过:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// ❌ 镜像断言:同一个 builder 算出了等式两边 —— 永远为真
|
|
24
|
+
const expected = buildSearchQuery({ tag: 'urgent' });
|
|
25
|
+
expect(buildSearchQuery({ tag: 'urgent' })).toBe(expected);
|
|
26
|
+
|
|
27
|
+
// ✅ 手工推导的字面量
|
|
28
|
+
expect(buildSearchQuery({ tag: 'urgent' })).toBe('tag:"urgent"');
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**不要写变更探测器。** 如果只有**有意为之的决定**才能让一个测试失败——某个常量的取值、某句消息的精确措辞、某个私有结构——那它会在重新设计时误报、却对真 bug 一路沉睡。要测那个**依赖于该决定的行为**:不是 `expect(MAX_RETRIES).toBe(5)`,而是"一次失败的调用会被重试 5 次,且第 6 次尝试永不发生"。
|
|
32
|
+
|
|
33
|
+
**测行为,不测文本。** 断言某个脚本、skill 或配置文件"包含某一行",只能证明源文件就是源文件。要拿受控输入去**跑**脚本,然后断言它的输出、副作用或退出码。用来指挥 agent 的文档,靠消费它的 agent 的行为来测(superpowers:writing-skills);写给人看的散文根本不该有测试。
|
|
34
|
+
|
|
35
|
+
**测你的代码,不测框架。** 测你的代码在其边界上所做的契约——你注册的那条路由、你发出的那条查询、你产出的那个 payload。上游的机制是它们维护者该写的测试(经典反例:断言你的 router 会调用一个已注册的 handler——那是框架的测试,不是你的)。当上游行为**确实**让你意外时,写一个窄窄的表征测试,把那个假设点名出来。同样的边界也适用于你代码内部:构造函数、getter、常量和琐碎的转发,只有当它们做校验、归一化、给默认值、做推导、做强制或产生副作用时才配有测试——否则就去断言第一个依赖于它们、且对消费者可见的结果。
|
|
36
|
+
|
|
37
|
+
### 门控函数
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
在写测试体之前:
|
|
41
|
+
点名那个会让这个测试失败的生产代码改动。
|
|
42
|
+
|
|
43
|
+
点不出来 → 围绕一个可观察的行为重新设计
|
|
44
|
+
"源文本变了" → 去跑这个产物,断言它的效果
|
|
45
|
+
只有有意为之的决定能让它失败 → 这是变更探测器;改测那个
|
|
46
|
+
依赖于该决定的行为
|
|
47
|
+
|
|
48
|
+
确认期望值的推导过程没有用到被测代码。
|
|
49
|
+
如果它复用了被测代码的逻辑或辅助函数:
|
|
50
|
+
换成字面量或手工核对过的 fixture
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 原则 2:跑真东西
|
|
54
|
+
|
|
55
|
+
**mock 不配拥有断言。** 一个针对 mock 的断言,在 mock 存在时通过、在 mock 缺席时失败——它对被测组件什么都没说。要断言**真实组件**的行为;如果你要检查的就是那个 mock,那就把它 unmock,或者把这条断言删掉。
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// ✅ 真实行为
|
|
59
|
+
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
|
60
|
+
|
|
61
|
+
// ❌ mock 是否存在
|
|
62
|
+
expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**你的人类伙伴会这样纠正你:** "我们是在测一个 mock 的行为吗?"
|
|
66
|
+
|
|
67
|
+
**在正确的层级上 mock。** 在替换真实方法之前,先搞清它的每一个副作用;只 mock 掉慢的或外部的那一步操作,把测试真正依赖的东西保留为真实的。不确定时,先拿真实实现跑一遍测试,观察实际上必须发生什么。
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// ❌ 这个 mock 吞掉了配置写入,而重复检测正是要读它
|
|
71
|
+
vi.mock('ToolCatalog', () => ({
|
|
72
|
+
discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
// ✅ 只 mock 掉缓慢的服务器启动;配置写入保持真实
|
|
76
|
+
vi.mock('MCPServerManager');
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**让替身足够具体。** 当参数、调用次数或调用顺序本身就是契约的一部分时,就要断言它们——一个什么都接受的 fake 什么都没验证。给每个分支(成功、报错、格式错误)配它自己的 fixture 或 spy,这样走错的分支就无法满足期望。
|
|
80
|
+
|
|
81
|
+
**完整镜像真实数据。** 按现实中的**完整结构**来 mock——所有有文档的字段——而不是只 mock 你这个测试会读的那几个。部分 mock 会静默失败:下游代码读到一个被省略的字段时,测试通过、集成崩掉。
|
|
82
|
+
|
|
83
|
+
**生产类只承载生产方法。** 只有测试才需要的清理逻辑,放在测试工具里,绝不作为生产类上的 `destroy()`。自问:这个方法只被测试调用吗?这个类拥有这份资源的生命周期吗?答错了 → 挪进测试工具。
|
|
84
|
+
|
|
85
|
+
**宁可用真实组件,也不要复杂 mock。** 当 mock 的搭建代码超过测试逻辑本身、mock 漏掉了真实组件才有的方法、或者 mock 一改测试就崩时,改成用真实组件的集成测试。**你的人类伙伴会这样问:** "这里我们真的需要用 mock 吗?"
|
|
86
|
+
|
|
87
|
+
### 门控函数
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
在添加 mock 或测试辅助函数之前:
|
|
91
|
+
列出真实方法的副作用;测试所依赖的那些保持真实 ——
|
|
92
|
+
只 mock 它们下面那一层「慢的/外部的」。
|
|
93
|
+
|
|
94
|
+
mock 的返回值要完整镜像真实结构。
|
|
95
|
+
|
|
96
|
+
只被测试调用的方法,属于测试工具,不属于生产代码。
|
|
97
|
+
|
|
98
|
+
正要对 mock 本身下断言?
|
|
99
|
+
把它 unmock,或者删掉这条断言。
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 测试与实现一同交付
|
|
103
|
+
|
|
104
|
+
TDD 循环——失败的测试、最小实现、重构——就是"完成"的定义。交付这个行为**需要**的测试,且只交付这些:琐碎代码和给人看的散文都不配有测试,而一个为了满足流程而写的测试会永远付出维护代价。
|
|
105
|
+
|
|
106
|
+
## 变异检查
|
|
107
|
+
|
|
108
|
+
收尾之前,在脑子里对生产代码做变异;对每一种现实的变异,都应至少有一个测试失败:
|
|
109
|
+
|
|
110
|
+
- 常量或参数写错
|
|
111
|
+
- 分支处理写错
|
|
112
|
+
- 缺失状态变更或副作用
|
|
113
|
+
- 返回空值或默认值
|
|
114
|
+
- 缺失对零值、空值、nil、未授权或格式错误输入的校验
|
|
115
|
+
|
|
116
|
+
一个没有任何测试能抓住的变异,标记出该行为无保护——或者那个测试是同义反复。
|
|
117
|
+
|
|
118
|
+
## 快速参考
|
|
119
|
+
|
|
120
|
+
| 当你…… | 就这么做 |
|
|
121
|
+
|--------|---------|
|
|
122
|
+
| 写任何测试 | 点名它要抓的破坏——是 bug,不是决定 |
|
|
123
|
+
| 构造期望值 | 手工推导;绝不用被测代码去算 |
|
|
124
|
+
| 测一个脚本或文档 | 跑它 / 压测它的消费者;绝不 grep 它的文本 |
|
|
125
|
+
| 想给依赖写测试 | 测你的边界契约,不测它们有文档的机制 |
|
|
126
|
+
| 想对一个被 mock 的元素下断言 | 改测真实组件,或者把它 unmock |
|
|
127
|
+
| 正要 mock 某个方法 | 先搞清它的副作用;在慢的/外部的那一层上 mock |
|
|
128
|
+
| 构造一个 mock 返回值 | 完整镜像真实结构 |
|
|
129
|
+
| 需要只有测试才用的清理逻辑 | 放进测试工具 |
|
|
130
|
+
| 眼看 mock 搭建代码膨胀 | 改成用真实组件的集成测试 |
|
|
131
|
+
| 写完一个测试文件 | 跑一遍变异检查 |
|
|
132
|
+
|
|
133
|
+
## 危险信号
|
|
134
|
+
|
|
135
|
+
- 搭建过程和断言共用同一个对象,等式必然成立
|
|
136
|
+
- 这个测试只可能因为 panic、崩溃或选择器缺失而失败
|
|
137
|
+
- 这个测试在每次有意改动时都失败,却从不在意外破坏时失败
|
|
138
|
+
- 期望值藏在循环、builder 或辅助函数背后
|
|
139
|
+
- 这个测试去 grep 源码文本,或者断言某个已删除的符号仍然是删除状态
|
|
140
|
+
- 就算只剩下框架,这个测试依然"成立"
|
|
141
|
+
- 这个测试是为覆盖率而存在的,不检查任何副作用或结果
|
|
142
|
+
- 某条断言检查的是 `*-mock` 这种 test ID,或者你把 mock 去掉它就失败
|
|
143
|
+
- 某个方法只被测试文件调用
|
|
144
|
+
- mock 搭建占了测试的一半以上,或者你说不出为什么需要这个 mock
|
|
145
|
+
- "为了安全起见"而 mock
|
|
@@ -1,33 +1,63 @@
|
|
|
1
1
|
# Gemini CLI 工具映射
|
|
2
2
|
|
|
3
|
-
Skills
|
|
4
|
-
|
|
5
|
-
| Skill
|
|
6
|
-
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
|
|
|
10
|
-
|
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
3
|
+
Skills 说的是动作("分派一个子智能体"、"建一条待办"、"读一个文件")。在 Gemini CLI 上,这些动作对应下面这些工具。
|
|
4
|
+
|
|
5
|
+
| Skill 请求的动作 | Gemini CLI 等价工具 |
|
|
6
|
+
|----------------|-------------------|
|
|
7
|
+
| 读取一个文件 | `read_file` |
|
|
8
|
+
| 一次读取多个文件 | `read_many_files` |
|
|
9
|
+
| 创建新文件 | `write_file` |
|
|
10
|
+
| 编辑文件 | `replace` |
|
|
11
|
+
| 执行 shell 命令 | `run_shell_command` |
|
|
12
|
+
| 搜索文件内容 | `grep_search` |
|
|
13
|
+
| 按名称查找文件 | `glob` |
|
|
14
|
+
| 列出文件和子目录 | `list_directory` |
|
|
15
|
+
| 抓取 URL | `web_fetch` |
|
|
16
|
+
| 搜索网页 | `google_web_search` |
|
|
17
|
+
| 调用一个 skill | `activate_skill` |
|
|
18
|
+
| 分派子智能体(`Subagent (general-purpose):` 模板) | `invoke_agent`,`agent_name: "generalist"`(也可用 `@generalist` 聊天语法调用——见[子智能体支持](#子智能体支持)) |
|
|
19
|
+
| 多个并行分派 | 同一条响应里发多个 `invoke_agent` 调用 |
|
|
20
|
+
| 任务跟踪("建一条待办"、"标记完成") | `write_todos`(状态:pending、in_progress、completed、cancelled、blocked) |
|
|
21
|
+
|
|
22
|
+
## 指令文件
|
|
23
|
+
|
|
24
|
+
当某个 skill 提到"你的指令文件"时,在 Gemini CLI 上指的是 **`GEMINI.md`**。Gemini CLI 按层级加载 `GEMINI.md`:全局的在 `~/.gemini/GEMINI.md`,项目级的在工作区目录及其各级父目录里,另外当某个工具访问子目录中的文件时,该子目录下的 `GEMINI.md` 也会被加载。
|
|
25
|
+
|
|
26
|
+
## 个人 skills 目录
|
|
27
|
+
|
|
28
|
+
用户级 skills 放在 **`~/.gemini/skills/`**,**`~/.agents/skills/`** 是跨运行时的别名目录(与 Codex、Copilot CLI 共用)。当同一层级下两个目录都存在时,`.agents/skills/` 优先。每个 skill 是一个子目录,里面有一份带 `name` 和 `description` frontmatter 的 `SKILL.md`。
|
|
29
|
+
|
|
30
|
+
## 子智能体支持
|
|
31
|
+
|
|
32
|
+
Gemini CLI 通过 `invoke_agent` 工具分派子智能体,该工具接收 `agent_name` 和 `prompt` 两个参数。同一个分派动作也有聊天语法快捷方式:输入 `@generalist <prompt>` 等价于以 `agent_name: "generalist"` 调用 `invoke_agent`。内置的 agent 名包括 `generalist`、`cli_help`、`codebase_investigator`,以及(启用浏览器工具后的)`browser_agent`。
|
|
33
|
+
|
|
34
|
+
Skills 用 `Subagent (general-purpose):` 来分派,并且要么引用一个提示词模板文件(例如 `superpowers:subagent-driven-development` 的 `./implementer-prompt.md`),要么直接给出内联提示词。在 Gemini CLI 上:
|
|
35
|
+
|
|
36
|
+
| Skill 里的分派形式 | Gemini CLI 等价做法 |
|
|
37
|
+
|------------------|-------------------|
|
|
38
|
+
| 引用某个 `*-prompt.md` 模板(implementer、task-reviewer、code-reviewer 等) | 把模板填好,然后以 `agent_name: "generalist"` 和填好的提示词调用 `invoke_agent` |
|
|
39
|
+
| 引用 `superpowers:requesting-code-review` 的 `./code-reviewer.md` | 以 `agent_name: "generalist"` 和填好的审查模板调用 `invoke_agent` |
|
|
40
|
+
| 内联提示词(没有引用模板) | 以 `agent_name: "generalist"` 和你的内联提示词调用 `invoke_agent` |
|
|
41
|
+
|
|
42
|
+
### 填写提示词
|
|
43
|
+
|
|
44
|
+
Skills 提供的提示词模板里有 `{WHAT_WAS_IMPLEMENTED}` 或 `[FULL TEXT of task]` 这类占位符。把所有占位符都填好,再把完整提示词交给 `invoke_agent`。模板本身就包含了该 agent 的角色、审查标准和期望的输出格式——子智能体会照着它执行。
|
|
45
|
+
|
|
46
|
+
### 并行分派
|
|
47
|
+
|
|
48
|
+
Gemini CLI 支持并行分派子智能体。在同一条响应里发出多个 `invoke_agent` 调用(或在一个提示词里写多个 `@generalist` 调用),即可让相互独立的子智能体工作并行跑。有依赖关系的任务保持串行,但**不要**为了让历史记录简单一点就把相互独立的子智能体任务串起来。
|
|
22
49
|
|
|
23
50
|
## Gemini CLI 额外工具
|
|
24
51
|
|
|
25
|
-
|
|
52
|
+
以下工具是 Gemini CLI 独有的:
|
|
26
53
|
|
|
27
54
|
| 工具 | 用途 |
|
|
28
55
|
|------|------|
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `ask_user` |
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
56
|
+
| `save_memory`(旧版) | 当 `experimental.memoryV2 = false` 时,跨会话持久化事实 |
|
|
57
|
+
| `get_internal_docs` | 查阅 Gemini CLI 自带的文档 |
|
|
58
|
+
| `ask_user` | 向用户提出结构化问题(文本 / 单选 / 多选) |
|
|
59
|
+
| `enter_plan_mode` / `exit_plan_mode` | 进入和退出只读的计划模式 |
|
|
60
|
+
| `update_topic` | 更新当前会话的主题 / 战略意图元数据 |
|
|
61
|
+
| `complete_task` | 表示某个 Gemini 子智能体已完成,并把结果返回给父 agent |
|
|
62
|
+
| `tracker_create_task`、`tracker_update_task`、`tracker_get_task`、`tracker_list_tasks`、`tracker_add_dependency`、`tracker_visualize` | 功能完整的任务跟踪器,支持依赖关系与可视化 |
|
|
63
|
+
| `read_mcp_resource`、`list_mcp_resources` | 访问 MCP 资源 |
|
|
@@ -1,299 +0,0 @@
|
|
|
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('renders sidebar', () => {
|
|
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('renders sidebar', () => {
|
|
43
|
-
render(<Page />); // 不要 mock sidebar
|
|
44
|
-
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// 或者如果必须 mock sidebar 来隔离:
|
|
48
|
-
// 不要对 mock 做断言——测试 Page 在 sidebar 存在时的行为
|
|
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('detects duplicate server', () => {
|
|
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('detects duplicate server', () => {
|
|
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
|
-
而非测试依赖的高层方法
|
|
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 完全匹配真实响应的结构
|
|
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 的 setup 比测试逻辑还长
|
|
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` test ID
|
|
287
|
-
- 方法仅在测试文件中被调用
|
|
288
|
-
- Mock setup 占测试的 >50%
|
|
289
|
-
- 移除 mock 测试就失败
|
|
290
|
-
- 无法解释为什么需要 mock
|
|
291
|
-
- "保险起见" mock 掉
|
|
292
|
-
|
|
293
|
-
## 底线
|
|
294
|
-
|
|
295
|
-
**Mock 是隔离的工具,不是被测试的对象。**
|
|
296
|
-
|
|
297
|
-
如果 TDD 揭示你在测试 mock 行为,你已经走偏了。
|
|
298
|
-
|
|
299
|
-
修复方法:测试真实行为,或质疑为什么要 mock。
|