---
title: "9.1 为什么需要测试"
description: "回归测试、测试金字塔、何时自动化——用小明的评分功能理解测试的核心价值"
chapter: "第九章"
---

# 9.1 为什么需要测试

> **本节目标**：理解回归测试的概念，掌握测试金字塔的分层逻辑，知道什么时候该引入自动化测试，以及如何让 AI 帮你写测试。

---

## "安全网"到底是什么

到了第九章，你可能已经在项目里见过测试文件了——Claude Code 在生成代码时有时会顺手创建 `*.test.ts`，或者你在别人的项目里看到过 `__tests__` 目录。但"见过测试"和"理解测试的价值"是两回事。小明就是这样：他知道测试这个概念，但一直觉得"我手动试试就行了，何必写测试代码"。直到他陷入了序言里的"打地鼠"困境——改评分坏搜索，修搜索坏详情页——他才开始认真想这个问题。

老师傅说他需要一张"安全网"。小明问："具体是什么意思？"

老师傅用小明的 app 举了个例子。小明的"个人豆瓣"现在有这些功能：电影列表、搜索、详情页、用户注册登录、电影评分。每个功能背后都有一些代码逻辑——搜索要查数据库、评分要写数据库、详情页要拼接多个数据源。这些功能之间并不是完全独立的，它们共享数据库连接、公共的工具函数、路由配置。改了其中一个地方，另一个地方可能就受影响。

老师傅画了一个简单的图：评分功能调用了 `db.query()` 这个公共函数来写入评分记录，搜索功能也调用了 `db.query()` 来查询电影。小明在给评分功能加"按时间排序"的时候，修改了 `db.query()` 的默认排序参数——他觉得这只影响评分，但搜索功能也在用这个函数，搜索结果的排序就被意外改变了。这种"改了 A 坏了 B"的情况，在代码库越来越大的时候会越来越频繁，因为模块之间的依赖关系越来越复杂，你很难在脑子里追踪所有的影响链。

"安全网"就是一组**自动化检查**。每个检查对应一个功能点：搜索"科幻"能返回结果吗？给电影打 4 星能成功吗？未登录用户访问评分接口会被拒绝吗？这些检查写成代码，存在项目里。每次你改完代码，运行一下这些检查——全部通过，说明你的修改没有误伤别的功能；某个失败了，说明你刚才的改动破坏了什么，赶紧去看。

这就是**回归测试**。"回归"这个词听起来学术，意思很朴素：**以前能用的功能，改了别的代码之后还能用吗？** 小明改评分逻辑导致搜索坏了，就是一个典型的回归 bug。如果他有一个搜索功能的自动化检查，改完评分代码跑一下，搜索的检查立刻报红——他当场就能发现问题，不用等朋友来报 bug。

人肉回归也能做——每次改完代码，打开浏览器，把所有功能手动点一遍。说实话，点一遍并不慢，十来个功能几分钟就能过完，而且手动测试有独特的价值：你在模拟真实用户的使用路径，能发现自动化测试覆盖不到的体验问题——"这个按钮点击区域太小了""加载完页面闪了一下""这个提示文案读起来很奇怪"。这些是机器感知不到的。

真正让人扛不住的不是"点一遍慢"，而是**重复**。小明今天改了评分逻辑，点一遍，没问题。下午又改了搜索排序，再点一遍。晚上修了一个样式 bug，又要点一遍。一天改三次代码就要点三遍，而且每次都要记住"上次点了哪些场景、这次不能漏"。第一遍你会认真点，第二遍开始走马观花，第三遍可能就跳过了"觉得不会出问题"的部分——而 bug 往往就藏在你跳过的那个部分。自动化测试解决的不是"手动测试太慢"的问题，而是**"每次都要完整地、不遗漏地重复"这件事，人做不到但机器做得到**。

而且人会做判断——"我只改了评分，搜索肯定没事"——然后跳过搜索的测试。这个判断大部分时候是对的，但偶尔是错的，而错的那一次就是 bug。机器不会做这种"聪明"的判断，你让它检查 15 个点，它就老老实实检查 15 个点，每次都一样。

还有一个容易被忽略的好处：**自动化测试是活的文档**。小明三个月后回来看自己的代码，已经记不清评分接口应该返回什么格式了。但测试文件里写得清清楚楚——`expect(res.status).toBe(200)`、`expect(res.body.averageRating).toBeCloseTo(4.0)`。测试用例比注释更可靠，因为注释可能过时（代码改了注释没改），但测试如果过时了会直接报红。

## 测试金字塔——不是所有测试都一样

小明理解了回归测试的价值，但新的问题来了："我该怎么写这些检查？是打开浏览器模拟用户点击，还是直接调用函数看返回值？"

老师傅说："两种都有，但它们的成本和价值不一样。"

用小明的评分功能来具体看。他的 app 里有一个函数 `calculateAverageRating`，接收一组评分数组，返回均分。比如输入 `[4, 5, 3]`，期望输出 `4`。测试这个函数非常简单：直接调用它，看返回值对不对。不需要启动服务器，不需要连数据库，不需要打开浏览器——纯粹的输入输出验证。跑一次几毫秒。这就是**单元测试**：测试最小的代码单元（一个函数、一个工具方法），速度极快，完全隔离。

单元测试的价值在于**精确定位**。如果 `calculateAverageRating` 的测试失败了，你立刻知道问题出在这个函数里——不用猜是数据库的问题还是网络的问题还是浏览器的问题。而且因为它不依赖任何外部环境，所以极其稳定——不会因为数据库连不上或者网络慢而"偶尔失败"。小明的 app 里适合单元测试的还有：搜索关键词的清洗函数（去掉特殊字符）、日期格式化函数、评分星级的显示逻辑（4.7 分显示几颗星）。这些都是纯输入输出的逻辑，不依赖外部环境。

往上一层。小明的评分功能有一个 API 接口 `POST /api/movies/42/rate`，接收用户提交的评分，写入数据库，返回更新后的均分。测试这个接口需要启动服务器、连接数据库（或测试数据库），发一个 HTTP 请求，检查返回的状态码和数据，再查数据库确认评分记录确实写进去了。比单元测试慢一些（需要网络和数据库），但它测的是"多个模块协作"的结果——路由解析、请求校验、数据库操作、响应格式化，这些环节串在一起能不能正常工作。这就是**集成测试**（也叫 API 测试）：测试模块之间的协作，速度中等，需要真实的依赖（数据库、服务器）。

集成测试能发现单元测试发现不了的问题。比如 `calculateAverageRating` 函数本身没问题，但 API 路由把用户提交的评分传给这个函数时，忘了把字符串转成数字——`"4"` 和 `4` 在 JavaScript 里行为不同。单元测试只测函数本身，发现不了这个"接缝"处的问题；集成测试从 HTTP 请求开始，经过完整的处理链路，能暴露这种模块之间的配合问题。

再往上。小明想测"用户给电影打分"的完整体验：打开浏览器，登录，找到一部电影，点击星星，提交评分，看页面上的均分有没有更新。这需要启动一个真实的浏览器，模拟鼠标点击和键盘输入，等待页面加载和渲染。跑一次可能要几秒甚至十几秒。而且它很"脆弱"——按钮换了个位置、加载慢了一点、弹窗挡住了元素，都可能导致测试失败，即使功能本身没问题。这就是 **E2E 测试**（端到端测试）：模拟真实用户的完整操作流程，最接近真实体验，但最慢、最脆弱、维护成本最高。

这三层构成了经典的**测试金字塔**。想象一下体检：验血是最基础的检查，快速、便宜，能发现大部分常见问题（单元测试）；B 超深入一些，检查器官之间的配合（集成测试）；全身 CT 最全面，但贵、耗时，不是每次体检都做（E2E 测试）。你不会每次感冒都去做 CT，同样，你不需要每个功能都写 E2E 测试。

金字塔的形状说明了数量分布：底层的单元测试最多（快、稳、便宜，多写无妨），中层的集成测试适量（覆盖核心接口），顶层的 E2E 测试最少（只测最关键的用户流程）。小明的评分功能，`calculateAverageRating` 这种纯函数用单元测试覆盖，`POST /api/movies/:id/rate` 这个接口用集成测试覆盖，"用户登录→找电影→打分→看均分"这个完整流程用一个 E2E 测试覆盖。大部分逻辑在底层和中层就能验证，不需要每个场景都启动浏览器。

为什么不反过来——全部用 E2E 测试，一步到位？因为成本差距巨大。一个单元测试跑几毫秒，一个 E2E 测试跑几秒到十几秒。小明的 app 如果有 50 个测试场景，全用单元测试跑完不到 1 秒；全用 E2E 测试可能要 5 分钟。而且 E2E 测试的维护成本高——UI 改了一个按钮的位置，所有涉及这个按钮的 E2E 测试都要改；但单元测试和集成测试完全不受影响，因为它们不依赖 UI。金字塔不是教条，而是经济学：用最低的成本获得最高的信心。

小明可能会问："那我怎么知道一个功能该用哪层测试？"一个简单的判断标准：**如果这个逻辑不依赖 UI 就能验证，就不要用 E2E 测试**。评分计算对不对？单元测试。接口返回的数据对不对？集成测试。用户点按钮能不能完成操作？这才需要 E2E。越往底层推，测试越快、越稳定、越便宜。

<TestPyramid />

<details>
<summary>好奇的话展开看看：三层测试的代码长什么样</summary>

```typescript
// 单元测试：测纯函数，不需要任何外部依赖
test('calculateAverageRating 计算均分', () => {
  expect(calculateAverageRating([4, 5, 3])).toBe(4)
  expect(calculateAverageRating([5])).toBe(5)
  expect(calculateAverageRating([])).toBe(0) // 边界：空数组
})

// 集成测试：测 API 接口，需要服务器和数据库
test('POST /api/movies/:id/rate 提交评分', async () => {
  const response = await request(app)
    .post('/api/movies/42/rate')
    .set('Authorization', `Bearer ${token}`)
    .send({ rating: 4 })

  expect(response.status).toBe(200)
  expect(response.body.averageRating).toBe(4)
})

// E2E 测试：模拟用户操作，需要浏览器
test('用户给电影打分', async ({ page }) => {
  await page.goto('/movies/42')
  await page.click('[data-testid="star-4"]')
  await page.click('[data-testid="submit-rating"]')
  await expect(page.locator('.average-rating')).toHaveText('4.0')
})
```

</details>

## 什么时候该引入自动化测试

小明被说服了，但他又问："我是不是现在就要把所有功能都写上测试？"

老师傅说："不一定。自动化测试是一笔投资——写测试需要时间，维护测试也需要时间。投入要能换来回报。"

什么时候回报明显？看看小明的情况：他的 app 有 5 个以上的页面，核心业务流程（搜索→详情→评分）超过 3 步，而且他已经遇到过"修 A 坏 B"的问题。这些信号说明他的项目复杂度已经超过了人肉回归能可靠覆盖的范围——该上自动化了。

<TestCoverageCalculator />

还有一个不那么明显但同样重要的信号：**你开始害怕改代码了**。小明知道搜索功能的代码写得不太好，想重构一下，但他不敢——万一改坏了呢？上次改评分就坏了搜索，这次改搜索会不会坏别的？这种"不敢动"的心态是代码腐化的开始——你知道代码有问题，但因为害怕引入新 bug 而不敢改，代码就越来越烂。自动化测试打破这个恶性循环：有了安全网，你可以放心重构，改完跑测试，绿了就没问题。

什么时候不该？如果你还在疯狂改需求的原型阶段，今天的评分功能明天可能整个砍掉换成点赞，那写测试就是浪费——测试跟不上需求变化，写了也白写。一次性项目（做完就不维护的活动页）、纯静态展示页（没有交互逻辑可测的），也不需要自动化测试。

还有一种常见的误区：**为了测试覆盖率而写测试**。有些工具会告诉你"你的代码覆盖率只有 30%"，你可能会觉得应该把它提到 80% 甚至 100%。但覆盖率是一个误导性的指标——它只告诉你"哪些代码被执行过"，不告诉你"测试有没有验证正确的行为"。你可以写一个测试调用了所有函数但不做任何断言，覆盖率 100%，但什么也没测到。相反，一个精心设计的测试可能只覆盖 20% 的代码，但恰好覆盖了最容易出错的 20%。关注测试的**价值**（它能不能帮你发现真正的 bug），而不是覆盖率的**数字**。

还有一个容易忽略的点：**先测什么**。小明不需要一口气给所有功能写测试。优先级很明确——先测核心业务逻辑（评分计算、搜索排序这些"算错了用户会发现"的功能），再测 API 接口（数据进出的大门），最后才考虑 E2E（只覆盖最关键的用户流程）。用 20% 的测试覆盖 80% 的风险，这比追求 100% 覆盖率实际得多。

一个实用的优先级判断方法：问自己"如果这个功能坏了，后果有多严重？"评分计算错了，用户看到错误的均分——严重，优先测。电影列表的排序方式从"按评分"变成"按时间"——不太严重，可以后面再测。404 页面的样式有点歪——无所谓，不用测。把有限的测试精力花在"坏了会很痛"的地方。

## AI 辅助测试的正确姿势

到了第九章，你已经让 Claude Code 帮你写了大量业务代码——从数据库 Schema 到 API 路由到前端页面。测试代码也一样，它完全能胜任。你告诉它"给评分接口写测试，覆盖正常、异常、边界场景"，它会分析你的接口代码，生成完整的测试文件，包括测试数据的准备和清理。

但测试和业务代码有一个关键区别：业务代码的需求通常写在 PRD 里或者你的描述里，AI 能直接理解；而测试的价值取决于"测的是不是关键场景"，这需要你对业务的理解。Claude Code 会读你的代码库，所以已经实现的逻辑它都能看到并测到——如果你的代码里有重复评分的处理逻辑，它会生成对应的测试。但如果一个业务规则还没实现、只存在于你的脑子里（比如"同一用户重复评分时应该更新旧评分而不是新增"，而你还没写这个逻辑），AI 就不会想到要测它。这种"还没写进代码的业务规则"是你需要补充的部分。

所以正确的分工是：**你把控测试策略，AI 执行和补充**。你可以让 AI 先分析代码库、建议测试优先级——它对代码结构的理解往往比你更全面。但最终"这个场景值不值得测""这个边界情况重不重要"的判断权在你手里，因为你比 AI 更了解业务上下文和用户行为。你审查生成的测试——不是看语法对不对（AI 不太会写错语法），而是看"有没有测到点子上"。漏了关键场景？补上。测了一堆不可能发生的情况？删掉，减少维护负担。

具体来说，审查 AI 生成的测试时关注两件事：**覆盖度**——四类场景（正常、校验、权限、边界）都覆盖了吗？尤其是边界场景，如果对应的业务规则还没实现在代码里，AI 不会凭空想到。**真实性**——测试用的数据和场景是否贴近真实使用情况？测试数据太"干净"（全是英文、全是正整数）可能漏掉真实环境中的问题（中文输入、特殊字符、浮点数精度）。

> 跟 AI 说："阅读我的项目代码，分析哪些模块最需要测试。列出建议的测试优先级：哪些先写单元测试，哪些需要集成测试，哪些需要 E2E 测试。"

这个 prompt 的价值在于：它让 AI 帮你做第一轮分析，但最终的优先级判断权在你手里。AI 可能会建议"所有 API 都需要集成测试"，但你知道有些接口是内部用的、几乎不会变，优先级可以降低。

---

::: tip 本节核心要点
- 回归测试的核心问题：以前能用的功能，改了别的代码之后还能用吗？
- 测试金字塔：单元测试多而快（测函数）、集成测试适量（测接口）、E2E 测试少而精（测流程）
- 自动化测试是投资：5+ 页面、核心流程 3+ 步、遇到过"修 A 坏 B"——这些信号说明该上了
- AI 帮你写测试代码，你负责判断"测的对不对"——业务特有的边界情况需要你补充
:::

::: info 下一步
理解了为什么需要测试和测什么，接下来去 [API 测试与 E2E 测试](./02-api-and-e2e-testing.md)——从小明的评分接口开始，实战 API 测试和 E2E 测试。
:::
