# Testing

> 需为代码写测试或改进脆弱测试时。

- Skill: `lion-1209/testing` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add lion-1209/testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lion-1209/testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Lion-1209 (https://skillmd.com/u/lion-1209)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lion-1209/testing

---


# Testing

## 概述

写能真正抓住 bug、且不脆弱（改实现不会无端崩）的测试。核心：测试是**行为的契约**，不是"代码的镜像"——测的是"给它这个输入，该有这个结果"，不是"它内部该这样工作"。好测试让你敢改实现（因为测试守着行为），坏测试让你不敢改（因为一改测试就崩）。

## 何时使用

- 写完功能，要补测试
- 写测试时纠结测什么、不测什么、要不要 mock
- 现有测试脆弱（改实现就一片红）或 flaky（时过时不过）
- 测试覆盖率挺高但 bug 还是漏

**不该用**：纯探索/原型（不要求正确，测试是负担）；一次性脚本（跑完即弃）。

**与相邻 skill 的衔接**：`testing` 在 task-breakdown 下游、verify-and-fix 上游——task 拆出"做完 X 能验证 Y"，testing 负责**把 Y 写成可重复运行的测试**，verify-and-fix 负责跑它验证。三者接力：task 定验证目标 → testing 把目标落地成测试 → verify-and-fix 用测试验证完成。

## 核心内容

### 先识别被测对象的性质

写测试前先看清"被测的是什么"，策略大不相同——这决定了要不要 mock、用什么工具、放哪个层次：

- **纯函数**（无副作用、输入决定输出，如 `calculateDiscount`）→ 直接输入输出断言，零 mock，单元层。
- **有外部依赖的逻辑**（如调 DB/HTTP 的 service）→ mock 掉外部副作用，验证被测逻辑对依赖返回值的真实处理（详见下文 mock 纪律）。
- **UI 组件**（渲染 + 交互）→ 用组件测试库测渲染输出和用户交互，不测内部 state 细节。
- **模块协作**（多个组件配合）→ 集成层，用真实（或内存版）依赖验证组件间契约。

识别性质能避免最常见的错配：给纯函数上 mock、给 UI 组件测 state、把单元能测的逻辑推到端到端。先问"它是什么"，再问"怎么测"。

### 测行为，不测实现

这是测试设计的第一原则。**测"做什么"，不测"怎么做"**：

- **测行为**（对）：给定输入，断言输出/可观测结果。例：`calculateDiscount(100, 'vip')` 应返回 `80`。
- **测实现**（错）：断言内部走了哪个分支、调了哪个方法几次。例：断言"内部调用了 `multiply` 两次"。

为什么测实现糟糕：**实现是会变的**（重构、换算法、优化），但行为不该变。测实现的测试，每次合理的实现改动都会让它崩——这就是脆弱测试。它逼你改实现时还得改测试，让测试从"保护"变成"负担"。

判别尺子：**问自己"如果我把内部实现整个换掉（但行为不变），这个测试还该过吗？"** 该过 → 测的是行为（对）；崩了 → 测的是实现（错，改）。

> 例外：有些"交互契约"本身就是行为——比如"调支付时确实发了请求""保存时确实写库了"。这类"验证发生了正确的外部交互"是测行为，不是测实现。区分点：你关心的是**结果**（钱扣了/数据存了），还是**调用细节**（调了 3 次不是 2 次）。前者是行为，后者是过度断言。

### 测什么：聚焦有判断的逻辑，跳过无价值的

不是每行代码都值得测。测试有价值，是因为代码**有逻辑、可能错**。按代码性质分：

- **有判断的逻辑**（分支、计算、状态转换、边界处理）→ 重点测。这是 bug 高发区。
- **纯数据搬运**（getter/setter、直接赋值、简单透传）→ 不值得专门测。测它等于测语言本身。
- **框架/库的代码** → 不测。你不需要测 ORM 的 save 有没有存数据库，那是框架的事。

判断尺子：**这段代码如果写错了，测试能抓住吗？写对了，测试有信息量吗？** 两问都否 → 不值得测（如 getter）。把测试预算投到"写错会出事"的地方。

**边界和错误路径是重点**：happy path 谁都会测，但 bug 大多藏在边界（空值、零、负数、空集合、最大值）和错误路径（异常、超时、依赖失败）。问自己"这个函数在什么输入下会出错？"——那些输入就是要补的测试。

### mock 的纪律：隔离依赖，不隔离被测逻辑

mock 用来隔离**外部依赖**（数据库、网络、第三方服务、时间），让测试快、稳、可重复。但 mock 容易被滥用：

- **合理 mock**：被测代码依赖的**外部副作用**（真连库太慢、真发邮件会骚扰人）。mock 掉它们，专注测被测逻辑。
- **过度 mock**：把**被测对象自己的依赖链**也 mock 掉，导致测试退化成"测 mock"——你 mock 了 db.save 返回固定 id，又只断言"调了 save"，那其实什么都没测。

判断尺子：**mock 之后，被测对象的真实逻辑还在被验证吗？** 还在（你对 mock 的返回做了真实处理和断言）→ 合理；不在（只是验证"调了 mock"）→ 过度，测试失去意义。

mock 的使用原则：

- **mock 边界，不 mock 内部**——mock 系统边缘的依赖（DB、HTTP），不 mock 被测代码内部的辅助函数
- **mock 行为，记录交互**——mock 要表达"依赖应该怎么响应"，而非"我猜被测会怎么调它"
- **少 mock**——能用真实组件（如内存数据库、内存文件系统）就别 mock，真实 > mock

### 一个测试一件事

每个测试函数聚焦**一个可验证的行为点**，断言精简：

- **差**：一个测试塞 10 个断言、测多个场景——失败时不知道哪条挂、改一条得动整个测试、名字没法概括（叫 `testEverything`）。
- **好**：一个测试一个明确的断言点，名字就是行为描述（`discountsVipBy20Percent`、`returnsOriginalPriceForUnknownLevel`）。

好名字的价值：测试失败时，**名字直接告诉你哪个行为坏了**，不用读测试代码。`test1 failed` 让你去看代码，`discountsVipBy20Percent failed` 直接定位问题。

### 测试金字塔：层次分明，多测便宜的

测试分三层，**数量比例应是金字塔**——底层多、顶层少，因为越往上越慢、越脆、越贵：

- **单元测试**（底层，最多）：测单个函数/类的行为，无外部依赖（依赖被 mock 或用真实轻量组件），毫秒级、跑得快。占绝大多数。
- **集成测试**（中层，适量）：测几个模块协作（如 service + 真实内存数据库），验证组件间契约。比单元慢，但比端到端快。
- **端到端测试**（顶层，最少）：测整条用户路径（如"从点击下单到支付成功"），最真实但也最慢、最脆（依赖多、易 flaky）。

常见误用：**倒金字塔**——端到端多、单元少。结果是测试套件又慢又脆，改一处一片红。判断尺子：**这个测试到底在测什么？**测纯逻辑 → 单元；测模块协作 → 集成；测用户能完成目标 → 端到端。能用单元测的别上集成，能用集成的别上端到端。

### 测试发现疑似 bug：先报告，别擅自当"行为"固化

写测试时常常发现代码行为可疑（如"负价居然照常打折""非数值返回 NaN"）。这时别擅自决定——有两种情况：

- **该测的**：这是**预期的当前行为**（哪怕怪）→ 写成测试契约化它，防止未来无意改动。
- **该报告的**：这是**潜在 bug**（行为不符合预期）→ 别急着写测试固化一个错误行为，先报告给代码作者/需求方确认。确认是 bug 就先修代码再写测试；确认是预期再固化。

判别尺子：**问"这个行为符合需求/常理吗？"** 符合（哪怕反直觉）→ 固化；不符合 → 报告，别固化 bug。把 bug 固化成"通过的测试"是最危险的——它给错误行为盖了"已验证"的章，未来谁想修都会被这个测试挡住。

### 测试结构：Arrange-Act-Assert

每个测试用三段结构，清晰可读：

```
// Arrange：准备输入和依赖
const service = new OrderService(mockDb);

// Act：执行被测行为
const result = await service.create(order);

// Assert：断言结果（行为）
expect(result.id).toBeDefined();
expect(mockDb.save).toHaveBeenCalledWith(order);  // 交互契约
```

三段分离让测试一眼能读懂"测了什么"。混在一起（准备、执行、断言交织）是测试难读、难维护的信号。

## 常见错误

| 问题 | 修法 |
|------|------|
| 测实现细节（断言内部调用次数/分支） | 改测行为，问"换实现行为不变，测试还该过吗" |
| mock 掉被测逻辑，退化成测 mock | mock 只隔离外部依赖，被测对象的真实逻辑仍要被验证 |
| 一个测试塞一堆断言 | 一个测试一个行为点，名字描述行为 |
| 测 getter/setter、测框架本身 | 把测试投到有判断的逻辑，跳过无价值的目标 |
| 只测 happy path | 重点补边界（空/零/负/空集合）和错误路径 |
| 追求覆盖率数字而非有效测试 | 覆盖率是必要不充分，从"什么 bug 漏了"反推该测什么 |
| 测试 flaky（时过时不过） | 多半是隐式依赖（时间/随机/顺序/共享状态），排查并隔离 |
| 测试层次倒金字塔（端到端多、单元少） | 金字塔分布：单元最多、集成适量、端到端最少；能下层测的别上上层 |
| 把发现的 bug 固化成"通过的测试" | 先报告确认——是 bug 先修代码再写测试，是预期行为再固化 |

