第 18 章 测试体系
副题:测试金字塔——"改坏了怎么办"从口头担忧变成真金白银
第 17 章交易系统上线了,五道防线立起来了。但一个新的问题立刻出现:**这些机制怎么证明它们是对的?**状态机的转移表怎么测?并发场景怎么复现?回调乱序、重复请求、对账差异——这些"防线的防线",靠什么守住?本章回答这个问题:测试体系——怎么证明"业务正确"不是一句口号。
第四部分地图(继续):16 性能管"快"、17 交易管"对"、本章(18)管"稳"的第一件——怎么证明"对"不是一次性的,是每次改动后仍然对(第 19 章安全管"防"、第 20 章质量管"久")——第四部分的四件事,本章是"证明"的那一件。
本章要建立的认知
- 测试的第一问是"回归":改坏了怎么办——测试存在的第一理由不是"证明对",是"改了之后还能跑";
- 测试金字塔:单元/集成/E2E 三层,越往下越多越快越便宜——投入按金字塔分配,不是平均用力;
- 测试是对业务正确的承诺:第 4 章的验收标准、第 7 章的契约、第 17 章的防线——每一个承诺都有一批对应的测试。
18.1 改坏了怎么办
阶段 3 的第一个"改坏了"事故,长这样:
支付回调处理里发现一个 bug(金额校验的顺序问题,第 17 章说过的回调处理函数),修它需要改一处逻辑。改完自测通过——回调的用例走了一遍,没问题。上线。第二天,对账(第 17 章最后一道防线)抓到了异常:下单接口的失败率涨了 10 倍。回滚,查原因:修回调的代码时,"顺手"动了一个两个函数共用的工具函数——下单流程也调用了它,被一起改挂了。
这个事故一点都不新鲜:改 A 崩 B——第 12 章一坨代码的经典症状,只是这次发生在交易系统里,代价是真金白银:10% 的下单失败,意味着几百笔订单没建成、几十笔用户支付了但订单没落库(第 17 章对账的兜底接住了,但客服电话已经响了)。
现场的完整链条也走一遍(它是"防线价值"的实景展示):改回调 → 共用工具函数被连带改挂 → 下单流程报错 → 对账第二天抓到差异 → 人工排查发现失败率飙升 → 回滚——第 17 章的五道防线,这次是最后一道(对账)接住了事故:钱没有长期错(对账抓到了)、用户损失可控(订单没落库的补单了)——防线不是防"不出错",是防"错了不可收拾"(第 17 章说过,这里是实景)。
复盘时第一个问题:为什么没早点发现?答案是:改完只验证了"改的那个功能"(回调处理),没验证"没改的功能"(下单流程)——而"没改的功能"才是大多数。这就是测试的第一课:测试存在的第一理由,不是"证明代码是对的",是"改完之后,还能跑"——这个"还能跑"的验证,叫回归测试(regression testing,回归=回到过去,确认没把过去弄坏)。
"为什么手工验证不够"也顺带回答(阶段 1 的直觉是"改完点一点就行"):手工验证的两个硬伤——记不住(阶段 3 的接口 12+ 个、状态机几十条转移、防线五道,人脑记不住全部"应该怎样",点一点只覆盖记得住的那几条);重复劳动(每次改动都要把同样的路径点一遍,点十次之后人就会"跳着点"——跳过的正好是出问题的那条)。测试的本质是把"每次改动都验证一遍"从人力变成代码——人力会累会跳,代码不会。
"为什么阶段 1 不建这套体系"也回答一句(它是复杂度预算在测试上的应用,第 4 章):阶段 1 的接口少、规则少、改错了代价低(内容错了改回来就行)——手工验证在阶段 1 是够用的;阶段 3 接口多了、规则密了、改错要赔钱了——测试体系是被业务逼出来的,不是提前设计的(第 2 章演进循环:先手工后自动化,和先一坨后分层同一个节奏)。业务越重,回归测试的需求越硬——这就是本章的全部起点:阶段 1 的"改坏了"是口头担忧,阶段 3 的"改坏了"是真金白银(第 17 章的错误成本对比)。
事故处置的三个动作也立一下(它把事故变成测试的养料):回滚(先恢复线上,第 23 章部署会说回滚是发布的第一退路)→ 修 bug(改回共用函数)→ 补测试(给"下单流程正常"写一条测试——让 bug 先红再修绿,证明修复有效且不再复发)——每一个线上事故,都应该变成一条新的测试:事故是"测试漏掉的承诺",补测试就是把这个承诺补回去(18.7 的"写测试时机"第一条)。
"补测试"的时机细节(它是事故处置里最容易跳过的):修复上线后立刻补,不要等"有空再说"——"有空再说"的测试永远补不上(修复上线了,事故被遗忘了,紧迫感消失了)——事故的记忆是测试的保质期:趁事故还新鲜,把承诺补回去。
回归的两个层面也先分清(18.7 会收):功能回归(功能行为没变——本章的主角)和性能回归(速度没变慢——第 16 章每周巡检的主角)——"改了还能跑"包含两层:还能跑对,还能跑快。
18.2 测试金字塔:为什么是金字塔
"多写测试"不是答案——写什么样的测试、写多少,才是答案。测试界有一个成熟的经验结构:测试金字塔(图 18-1)。
三层各有各的职责:
- 单元测试(unit):测一个函数/一个类的规则——业务层的校验、状态机的转移、金额的计算。不碰数据库、不碰网络、不碰 HTTP。特点:快(毫秒级,一次跑几千个)、便宜(不依赖环境)、好定位(挂了就知道是哪个函数);
- 集成测试(integration):测多个组件合起来——服务层 + 真实数据库、接口层 + 契约。特点:中速(秒级)、真环境(数据库是真的)、好覆盖(层与层的配合问题只有合起来才测得出);
- E2E 测试(end-to-end):测完整旅程——从 HTTP 请求到数据库到响应,模拟真实用户(第 15 章的联调清单自动化版)。特点:慢(秒到分级)、贵(要起整个系统)、最接近真实(但挂了难定位:可能是前端、后端、数据库任何一处)。
**为什么是金字塔而不是三层平均?**三个理由,都是从"成本与速度"长出来的:速度决定反馈——单元测试毫秒级,改完立刻跑,开发时用;E2E 分级,跑一次要等,上线前用——反馈越快,用得越频繁,所以单元测试应该最多;成本决定数量——E2E 要起整个系统,每加一个都贵,只能精挑细选(核心路径才配);定位决定层级——单元挂了立刻知道是哪行代码,E2E 挂了要从头查——越多的问题在越底层被发现,排查成本越低。
金字塔与第 18 章开头事故的呼应(事故是理解金字塔的最佳入口):18.1 的"改 A 崩 B"——回调函数有单元测试吗?有(它自己的规则测了);但"共用函数被改挂"是下游影响,单元测试不知道——这个事故能挡住它的层是集成/E2E(下单流程的测试):如果有一条约"下单流程正常"的 E2E,改动后跑一遍就红了——金字塔不是理论,是 18.1 事故的答案。
所以金字塔的形状是结论不是审美:底层多、顶层少。常见的新手错误是把金字塔倒过来——只写 E2E("端到端才真实")或只写单元("单元够快")——前者慢到不想跑,后者漏掉集成问题。金字塔的比例本身就是决策(18.7 用九步走一遍)。
金字塔是经验结构不是教条(出处一句话:测试界广泛引用的 Mike Cohn 的经验总结):它的形状跟着"你的成本结构"变——规则密集的业务(交易)单元层更厚,集成风险高的系统(服务间通信多)中层更厚——金字塔是起点,比例是决策(18.7)。
三层与本书前面章节的对应(这是测试章最顺的一条路):单元测试测的是第 12 章的业务层(规则的家);集成测试测的是第 12 章的链路(中间件+控制器+服务+数据库);E2E 测的是第 15 章的完整旅程——第 12 章分层的时候说过"没有分层就没有单独测一层的可能",现在就是兑现。
金字塔与第 15 章联调的关系也收一句(两个"手工→自动"的对比):联调是"人照着清单点"(第 15 章),E2E 是"机器照着断言跑"(本章)——联调发现问题,测试防止问题(第 15 章说过):联调暴露的每一个问题,都应该变成一条 E2E/集成测试(18.1 的"事故变成测试"在联调场景的版本)——第 15 章的手工清单是测试的原料,本章的测试是清单的自动化。
问题在哪层发现,也按层归位(一张小表,排障时有用):
| 问题类型 | 哪层能发现 | 哪层最早发现 |
|---|---|---|
| 业务规则错(校验/计算/状态) | 单元/集成/E2E | 单元(最快最准) |
| 契约不一致(字段/状态码) | 集成/E2E | 集成(契约测试) |
| 权限漏洞(401/403 错) | 集成/E2E | 集成(越权测试) |
| 链路不通(各自能跑合起来挂) | E2E | E2E(唯一能测的层) |
"最早发现"的层就是该写测试的层——这张表反过来读,就是"什么测试写在哪层"的答案。
测试的另一个维度也补一句:自动 vs 手动——金字塔讲的是自动化测试的分层,但手动测试没有消失(探索性测试:人随机地乱点乱试,机器想不到的路径它想到;上线前的人工走查:第 15 章联调清单的最后一次人工执行)——自动化管"每次都测",手动管"没想到的"——两者不是替代,是分工(自动化覆盖已知,手动探索未知)。
测试与代码评审(第 20 章的预告,先立关系):评审时看什么?——看测试有没有覆盖承诺(新功能:验收标准变成测试了吗?改 bug:复现的测试补了吗?)——测试是评审的检查单:代码可以看不懂,测试覆盖不全一眼就能看出(第 20 章会说评审是质量的第一道闸,测试覆盖是它的第一项)。
测试代码的组织(一句,够用):测试目录按金字塔分层放(tests/unit、tests/integration、tests/e2e),命名对齐被测模块——测试的目录结构就是金字塔的物理形态(第 12 章分层的思路,测试也分层);框架名点到为止(前端 Jest、后端 pytest 一类——机制学会了,换框架只是换 API 名,第 9 章那句话的又一次应用)。
18.3 底座:单元测试——测业务规则
金字塔的底座,从第 12 章的地基开始。第 12 章说过:分层让"单独测一层"成为可能——单元测试测的就是业务层:直接调用 service 函数,传参数、看返回,不碰 HTTP 不碰数据库:
// 单元测试:业务层规则(第 12 章分层的地基兑现)
it('内容标题超过 100 字应拒绝', () => {
const result = contentService.createContent(1, { title: 'x'.repeat(101), body: 'ok' });
expect(result.error.code).toBe('INVALID_TITLE');
});
it('内容合法应创建成功', () => {
const result = contentService.createContent(1, { title: '相机', body: '9 成新' });
expect(result.data.id).toBeDefined();
});
单元测试测的是"规则":校验规则(标题长度)、业务规则(谁能删——第 13 章权限表)、计算规则(金额——"分"存储铁律)。再补两个案例(它们是最典型的"规则"):
// 单元测试:金额计算(第 17 章"分"存储铁律的测试)
it('金额加法不丢精度', () => {
expect(addFen(100, 99)).toBe(199); // 1 元 + 9 角 9 分 = 1 元 9 角 9 分,整数加法
});
// 单元测试:授权规则(第 13 章"谁能删"在业务层)
it('非作者删除内容应拒绝', () => {
const result = contentService.deleteContent(2, 10); // 用户 2 删内容 10(作者是 1)
expect(result.error.code).toBe('FORBIDDEN');
});
"分"存储的测试价值(第 17 章 1 分钱事故的预防):浮点误差是"算出来的 bug",测试最容易覆盖(给输入断言输出)——金额计算的每一行,都该有测试(第 17 章对账是事后抓,单元测试是事前挡)。第 17 章的状态机转移表也是单元测试的天然素材——每一条合法转移测一遍、每一条非法转移测一遍(CANCELED 收到支付回调必须抛错):
// 单元测试:状态机转移表(第 17 章第一道防线的测试)
it('CANCELED 收到支付成功回调必须拒绝', () => {
expect(() => transit({ status: 'CANCELED' }, 'pay_success')).toThrow();
});
it('PENDING 支付成功转入 PAID', () => {
expect(transit({ status: 'PENDING' }, 'pay_success')).toBe('PAID');
});
单元测试的三个优点(它们决定了金字塔的底座为什么最大):快——毫秒级,一次跑几千个,改完代码立刻全量跑一遍(这就是"回归"的日常形态:每次改动,几千个规则在几秒内替你确认"没改坏");准——挂了立刻知道是哪个函数哪条规则,不用查;稳——不依赖环境(不连数据库不连网络),任何机器上跑结果一样。
三个优点的反面就是它的盲区(诚实交代,对应金字塔中层存在的理由):单元测试不知道"合起来"对不对——契约对不对(字段名)、权限通不通(中间件挂没挂)、事务真不真(回滚没回滚)——单个函数都对,合起来错,这正是 18.1 事故的形态(回调函数单独测没问题,共用函数被改挂的是下游)——单元测试的"准"是以"窄"换来的:它精确地告诉你"这个函数坏了",但"这个函数坏了影响谁"要集成测试回答。
单元测试的边界:测行为,不断言实现——断言"标题超过 100 字返回错误"(行为),不断言"内部调用了 validateTitle 函数"(实现细节)——实现细节今天长这样,明天重构就变了(第 20 章的重构),断言实现的测试在重构时全红,而且红的没有意义(行为没变,实现变了)——单元测试是重构的保镖:它确认"行为没变",前提是它只测行为(18.7 的重构衔接,这里先立规矩)。
依赖怎么隔离(单元测试不碰数据库的前提,第 12 章分层的红利):业务层不依赖数据库(第 12 章说过的分层规则——数据访问层管存取),所以单元测试直接调 service、传参数、看返回——分层让"不用数据库也能测业务规则"成为可能;遇到业务层真的依赖的东西(比如发通知的第三方),用测试替身(mock/stub:一个假的实现,记录被调用了、返回预设结果)——替身的原则:替掉"外部",不替"规则"(替掉网络/数据库/时间,不替校验/计算/状态)。
第 9 章的组件测试也是单元测试的一种(前端版):组件是"状态 → 界面"的纯函数(第 9 章说过 f(状态)=界面),测组件就是给状态断言界面——第 9 章说"可测试性"时埋的伏笔,这里是它的兑现。
测试命名是文档(测试可读性的第一纪律):测试名应该是一句完整的话——"标题超过 100 字应拒绝"比 "test1" 有用一百倍——测试挂了,测试名就是第一份错误报告("标题超过 100 字应拒绝"红了,不用看代码就知道是什么规则被破坏);测试名写不清楚,说明这条规则本身没想清楚。
18.4 中层:集成测试——契约与权限的考场
金字塔的中层,测"合起来才对"的东西。两个最重要的集成测试对象,都是前面章节埋的伏笔:
契约测试(第 7 章 + 第 15 章的兑现):第 7 章说过"契约是测试用例的骨架",第 15 章说过"联调清单的自动化版是契约测试"——现在实现:对每个接口断言"返回结构符合契约"(字段在、类型对、状态码对、错误码对):
// 集成测试:契约测试(第 7 章契约 = 测试骨架)
it('GET /v1/contents 返回结构符合契约', async () => {
const res = await api.get('/v1/contents');
expect(res.status).toBe(200);
expect(res.data.items[0]).toMatchShape({ // 契约里定义的字段
id: 'number', title: 'string', author_name: 'string',
});
});
契约测试的价值:它把第 15 章"联调才发现字段对不上"的问题前置——后端改了返回结构,契约测试立刻红,不用等前端联调时爆炸。契约测试是联调问题的自动化防线(第 15 章说过,这里是执行)。
契约测试与版本化的衔接也一句(第 7 章的"v1 不破坏、新东西进 v2"):契约 v2 上线时,契约测试跟着 v2 走(v1 的测试保留——旧调用方还在用 v1,v1 的承诺还在)——接口版本化的双轨,在测试里就是两套契约测试并行(第 7 章说"接口是承诺",测试就是承诺的账本:v1 的账没销,v1 的测试不能撤)。
越权测试(第 13 章的兑现):第 13 章说过"权限表是最好的测试用例清单——每个接口三问:没登录(401)?登录了没权限(403)?正常(200)?"——现在批量实现:
// 集成测试:越权测试(第 13 章权限表 = 测试清单)
it('未登录删除内容返回 401', async () => {
const res = await api.delete('/v1/contents/10'); // 不带凭证
expect(res.status).toBe(401);
});
it('非作者删除返回 403', async () => {
const res = await api.delete('/v1/contents/10'); // 带小明的凭证,删老周的内容
expect(res.status).toBe(403);
});
集成测试的"真"是它的价值也是它的成本:数据库是真的(用测试库,不是生产库)、HTTP 是真的(起一个测试实例)——所以它能测出"合起来才暴露"的问题(契约不一致、权限漏洞、事务边界);但也因此比单元测试慢、比单元测试脆(依赖环境就绪)——这就是金字塔中层不能太多的原因。
集成测试还有一个第 16 章的兑现:缓存逻辑——缓存穿透/失效是"合起来才暴露"的问题(缓存代码 + 数据库 + 并发):集成测试验证"缓存未命中时查库并写回""缓存命中时不再查库""TTL 到期后重新加载"——第 16 章的三场事故(失效/穿透/雪崩)在测试里的形态:每条缓存规则一条断言,事故的预防从"出事后调"变成"上线前测"。
日志与可观测性的测试(第 25 章的预告,一句):测试里也能断言"关键路径打了日志"(下单成功有 order_no 日志)——日志是排障的原料(第 25 章),原料缺失的 bug 排起来最痛——"日志没打"也是一种缺陷,该有一条断言。
事务边界是集成测试的另一个对象(第 14 章事务的第 18 章兑现):单元测试测不到"事务真的回滚了"(那要数据库配合)——集成测试验证:事务里抛错,数据不落库(下单事务:扣库存后建订单失败,库存必须还在):
// 集成测试:事务回滚(第 14 章 ACID 的验证)
it('建订单失败时库存回滚', async () => {
await forceOrderInsertError(); // 让建订单抛错
const stock = await db.query('SELECT stock FROM product WHERE id=101');
expect(stock).toBe(10); // 库存没被扣(回滚了)
});
测试数据库的维护(集成测试的日常成本):测试库要跟着第 6 章的迁移走(表结构变了,测试库也要迁移);每轮测试前重置数据(第 15 章说过的 seed 脚本——测试数据已知,断言才可靠)——测试库的"干净"是集成测试可靠性的前提(脏数据会让测试偶发红,偶发红的测试最后会被无视——"狼来了"效应)。
18.5 塔尖:E2E——完整旅程与重放
金字塔的塔尖,数量最少、最贵,测的是完整旅程。第 15 章的联调清单(登录→发内容→列表→评论→删除→登出)——E2E 就是它的自动化版:模拟真实用户走完整条路,断言每一步的结果:
// E2E:完整旅程(第 15 章联调清单的自动化)
it('用户下单完整旅程', async () => {
await login('xiaoming'); // 登录
const order = await createOrder('camera-101'); // 下单
expect(order.status).toBe('PENDING');
await payCallback(order.order_no, 'SUCCESS'); // 支付回调(模拟渠道)
expect(getOrder(order.order_no).status).toBe('PAID'); // 订单推进
});
E2E 的价值:它是唯一"从用户视角"的测试——前端、后端、数据库、网络全链路一起验证,第 15 章"各自能跑连起来不通"的问题只有它能兜住;E2E 的代价:慢(每次跑分钟级)、贵(环境要起全套)、脆(任意一层波动都可能导致假失败)——所以 E2E 只覆盖核心路径(下单、支付回调、登录——第 17 章交易的核心旅程),不追求覆盖所有细节。
E2E 测什么的判断标准也立一个(它是"核心路径"的可操作定义):用户高频走的、出错了代价大的——下单、支付、登录属于两者兼具(高频+贵);"修改头像"高频但便宜(E2E 不测,单元/集成够);"对账"低频但极贵(E2E 不测——它有专门的脚本,第 17 章)——E2E 只收"高频且贵"的路径,其余交给下层。
E2E 的"脆"怎么应对(它是 E2E 实践的第一课):等待策略——页面加载/网络请求是异步的,E2E 里最常见的假失败是"断言跑在了结果之前"(元素还没渲染就断言,红了;人看的时候又好了)——修法:显式等待(等到元素出现再断言,而不是固定睡 2 秒);隔离外部——E2E 依赖的第三方(支付渠道模拟器、短信网关)用可控的替身,外部一波动 E2E 就"狼来了"。E2E 的稳定性是维护出来的,不是写完就有的——它也是金字塔顶层"贵"的一部分:写一条 E2E 是一小时,维护它不 flaky(忽红忽绿)是持续的投入——这也是"E2E 只能少"的另一个理由。
E2E 的另一个形态:上线后的健康检查(第 25 章可观测性的预告):把核心路径的 E2E 部署到生产环境定时跑——用户报障之前,E2E 先发现"下单挂了"——这就是"合成监控"(第 25 章会展开):测试不只是上线前跑,也上线后跑——E2E 从"开发工具"变成"运维哨兵"。
E2E 的数据策略(它是 E2E 可靠性的另一半):E2E 在测试环境用 seed 数据(第 15 章的造数脚本);上线后的合成监控用专用测试账号(不污染真实用户数据、不被用户数据干扰)——E2E 的数据要"可控":数据不可控,断言就不可靠("订单列表第一条"到底是哪条?)。
第 17 章的重放测试(承诺的兑现)——它是 E2E 的变体,专测幂等防线:把同一个下单请求发两次,断言只产生一个订单;把同一个回调发三次,断言订单状态只被推进一次:
// E2E:重放测试(第 17 章幂等防线的验证)
it('同一 order_no 下单两次只产生一个订单', async () => {
const orderNo = 'order-20260815-001';
await createOrder('camera-101', orderNo);
const second = await createOrder('camera-101', orderNo); // 重放
expect(second.order_no).toBe(orderNo);
expect(countOrders(orderNo)).toBe(1); // 只有一个订单
});
重放测试为什么重要(第 17 章说过"成本最低收益最高"):它测的是"防线的防线"——幂等逻辑写错了,平时看不出来(正常请求都是单次),只有重放才暴露。交易系统的测试必须专门设计(第 17 章说过):普通功能测"正常路径",交易系统额外测"重复、乱序、丢失"——这是第 17 章错误清单(重复/丢失/缝隙)的测试版。
错误清单的测试版展开(三个根源各对应什么测试):重复——重放测试(同一请求发两次,18.5 的代码);乱序——状态机测试(取消后回调到达,单元测试覆盖非法转移,18.3 的代码);丢失——主动查询测试(回调不来的订单,轮询后状态更新——集成/E2E 测"丢回调→定时任务补上"的路径)——第 17 章的每一行错误清单,在测试章都有一行对应的测试:错误清单是"环境的不可靠清单",测试就是"针对不可靠的演练"。
18.6 测试与需求:承诺的映射
三层都建好了,退一步看全局:测试到底在测什么?——答案是:测试在测承诺。把本书的承诺清单翻出来,每一份承诺都对应一批测试(图 18-2):
映射表的文字版(它也是"测试计划"的骨架):
| 承诺(章节) | 测试形态 | 例子 |
|---|---|---|
| 验收标准(第 4 章) | 单元/重放测试 | "标题≤100 字"→一条断言;"重复点击只产生一个订单"→重放 |
| 接口契约(第 7 章) | 契约测试 | 返回结构/状态码/错误码逐字段断言 |
| 权限表(第 13 章) | 越权测试 | 每个接口三问:401/403/200 |
| 交易防线(第 17 章) | 防线测试 | 状态机转移表/重放/对账脚本——"防线的防线" |
这张表就是测试计划的完整形态:不是"给代码写测试",是"给承诺写测试"——承诺在哪里,测试就在哪里;承诺新增(新功能),测试跟着新增;承诺变更(需求变了),测试跟着改(18.6 的"测试坏了先判断谁坏了")。
"承诺漏了测试"的信号(它告诉你什么时候该补测试计划):线上事故(18.1:事故=漏掉的承诺,补测试);评审问不出问题(第 20 章的预告:评审冷场往往是测试覆盖不全——没有测试可看,只能看代码)——两个信号,都是"承诺与测试脱节"的症状(第三个信号"重构不敢做",18.7 的"测试与重构"会讲——重构的胆量来自测试)。
"测试是对业务正确的承诺"(方案原话):业务方说"这个功能要这样",验收标准把它变成可测的句子(第 4 章),测试把它变成可跑的代码——测试是承诺的代码形态:承诺在文档里会被忘记,在测试里不会(每次改动都会重新验证它)。
测试计划的维护节奏(它是活文档,和第 7 章契约同款):测试计划跟着承诺走——每轮迭代开始,对照需求清单(第 4 章)新增/修改测试计划;迭代结束,测试报告(哪些通过、哪些没测、新增了什么承诺)是评审的输入——测试计划是承诺的账本,账本要跟上业务的节奏。
测试挂了的处理顺序(它是测试体系的行为准则):先判断"是测试坏了还是代码坏了"——需求变了,测试该改(承诺变了,旧的测试作废);代码没照承诺做,改代码(承诺没变,代码违约)。这个判断错了,测试就失去意义:需求变了不改测试(测试永远红,然后被无视),或代码违约时改测试(测试跟着代码走,等于没有承诺)——测试是承诺的守门员,守门员不能跟着球员跑——这句也是本章的"测试观"总结:测试的立场是承诺,不是代码(代码变了测试不一定要变,承诺变了测试才变);立场错了,测试体系就退化成"代码的复读机"——复读机式的测试(代码怎么写的就怎么断言)永远不会红,也永远没有用。
新需求的流程(第 4 章流程的测试化,把"测试先行"变成团队习惯):需求 → 验收标准(第 4 章)→ 测试(本章)→ 代码——验收标准写下的那一刻,测试的骨架就定了(每条验收标准一条断言)——先写测试再写代码(TDD 的团队版)的好处:写代码时目标明确(让测试绿),写完立刻有回归保护;至少也要做到"代码和测试一起提交"(测试不是事后补的,是交付的一部分)——验收标准 → 测试 → 代码,是第 4 章流水线的测试版。
测试失败的可见性(它决定测试有没有用):测试失败必须显眼——CI 里红灯(第 22 章:提交代码自动跑测试,红了不能合并)——测试的威慑力来自"红了大家都知道":一个人悄悄改测试绕过红灯,测试体系就名存实亡——测试的纪律问题(红了怎么办)和测试的技术问题(怎么写)同等重要。
18.7 测试投入的决策:金字塔比例
测试写多少?这是测试章最后一个问题,也是一个典型的决策问题——走一遍九步(第 5 章模型的第 11 次应用):
① 业务目标:改动了代码,能快速确认"没改坏"(回归);② 业务约束:阶段 3 团队 2-3 人、交易功能刚上线(第 17 章防线要守住)、接口 12+ 个、内容+交易两条核心路径;③ 技术问题:测试投入怎么分配?④ 候选:A 金字塔(单元为主+集成+少量 E2E);B 倒金字塔(E2E 为主);C 只写单元;D 只写 E2E;⑤ 评价维度:反馈速度(改动后多久能确认)、覆盖深度(问题能挡住几层)、维护成本(测试代码也是代码);⑥ 打分:B 反馈慢(E2E 分钟级,改完等不起)、C 漏集成问题(契约/权限测不到)、D 慢且贵且脆——A 三围均衡:快反馈(单元为主)+ 深覆盖(集成补配合)+ 成本可控(E2E 只覆盖核心路径);⑦ 选择:A——参考比例:单元 70%/集成 20%/E2E 10%(比例是起点不是教条,按项目的"规则密度"调:业务规则多则单元更多,集成风险高则集成更多);⑧ 收益:回归安全(改动后几分钟内确认)、重构底气(第 20 章重构靠的就是测试兜底);⑨ 代价:测试代码也是负债——测试要维护(需求变了测试跟着改)、要运行(CI 里跑,第 22 章)、要排查(测试挂了要查是代码还是测试)——测试不是免费的,写测试省的是"未来返工"的钱,不是"现在不写"的钱。
测试与重构的关系(第 20 章的预告,这里先把逻辑立住):重构的定义是"行为不变、结构变"——怎么证明"行为没变"?只有测试——没有测试的重构是"重写"(行为变没变没人知道);测试是重构的前提,不是结果——先有测试,才谈得上重构(第 20 章展开:技术债的偿还靠重构,重构的胆量来自测试)。
TDD 一句话(先红后绿,它常被当作测试的代名词):测试驱动开发=先写测试(红)→ 写实现让它绿(绿)→ 重构——它是"测试先行"的工作流选择,不是测试的必须形态(补测试也可以,先写测试也可以)——第 20 章协作语境会讨论哪种适合你的团队,本章只需要知道:TDD 是"让测试成为设计工具"的一种用法。
什么该测什么不该测(边界,也是测试经验最值钱的部分):
- 该测:业务规则(校验/计算/状态转移)、契约(接口结构)、权限(401/403)、核心路径(下单/支付);
- 不该测(或低优先级):框架的行为(React 怎么渲染、Express 怎么路由——框架自己测过了,你测它是在测第三方);纯展示细节(样式、文案);一次性脚本(跑完就扔的迁移/修复脚本——第 4 章复杂度预算在测试上的应用);
- 覆盖率一句话(它常被误用):覆盖率是"测了多少"的度量,不是"质量多高"的保证——100% 覆盖率的烂测试(只断言"不报错")比 50% 覆盖率的真测试差——覆盖率看趋势不看数字。
"不该测"的边界还要防一个反向错误(它是"框架不用测"的误用):不用测框架,但要测"我们用框架的方式"——第 9 章说"框架内建常见问题答案",我们用框架写的业务逻辑(组件逻辑、中间件配置)是我们的代码,该测——边界是:测自己的,不测别人的。
测试的时间预算(写测试花多少时间——新手最关心):经验区间是开发时间的 20-30%(写 2 小时代码配 30-40 分钟测试)——它不是"额外开销",是把将来返工的时间提前花(18.7 的"省未来返工的钱");比例过低的信号:bug 反复修(每次都是同一个问题);比例过高的信号:测试比代码难维护(该考虑"测试代码也是负债"了)。
回归还有性能版(第 16 章的衔接):功能回归是"功能没改坏",性能回归是"速度没变慢"——第 16 章的每周巡检(命中率/慢查询/P95 对预算)就是性能回归测试——测试金字塔管功能,性能巡检管速度,两个都是"改了还能跑"。
写测试的时机(新手最常问):改 bug 时先写测试(让 bug 先红,再修绿——证明修复有效且不再复发);新功能写完补上(先写后写都行,关键是上线前要有);重构前确认已有测试(没有就先补核心路径的)——三个时机对应三个目的:防复发、防漏测、防重写——测试的时机跟着风险走:风险最高的地方(bug 过的、核心路径)测试最先到位。
测试的进化路径(第 15 章说过的,现在完整):纸面清单(第 15 章联调)→ 手工脚本(第 17 章重放测试的雏形)→ 本章的分层测试 → 第 22 章的 CI(提交代码自动跑)——测试的自动化程度跟着团队走,先有测试,再谈自动化——这也是"先跑通后组织"(第 12 章)在测试上的又一次实例:先让测试存在,再让测试成体系——和先一坨后分层是同一个节奏(第 2 章演进循环的又一处回响)。
测试投入的决策记录(第 5 章那张表的新一行):测试=金字塔分层(单元 70/集成 20/E2E 10),演进条件=团队变大或规则密度上升时重新分配——写进决策记录的理由和前面一样:比例将来会被质疑("E2E 太少了!"),记录里存着当时的理由——决策记录是决策的账本(第 12 章说过,这是第 11 次填写,和第 16 章缓存决策同页)。
18.8 本章对应表
| 业务诉求 | 技术选择 | 为什么 | 代价/取舍 |
|---|---|---|---|
| 改坏了怎么办(回归) | 测试金字塔(单元/集成/E2E) | 改动后确认"还能跑",业务越重需求越硬 | 测试代码也是负债(维护/运行/排查) |
| 快速反馈 | 单元测试为主(金字塔底座) | 毫秒级、好定位、不依赖环境 | 测不到"合起来"的问题 |
| 契约/权限合起来才对 | 集成测试(契约/越权) | 第 7 章契约+第 13 章权限表的测试化 | 依赖真环境,比单元慢 |
| 完整旅程必须通 | E2E(核心路径) | 第 15 章联调清单自动化,用户视角唯一 | 慢/贵/脆,只覆盖核心 |
| 交易防线不能破 | 防线测试(状态机/重放) | 第 17 章错误清单的测试版 | 需要专门设计(重复/乱序/丢失) |
| 测试写多少 | 九步决策(金字塔比例) | 反馈速度/覆盖深度/维护成本三围均衡 | 比例是起点不是教条 |
每一行都在本章正文里有完整论证:回归事故在 18.1,金字塔在 18.2,单元在 18.3,集成在 18.4,E2E 在 18.5,映射在 18.6,决策在 18.7。先看"业务诉求"列——这一章的每一行,都是从"改 A 崩 B"那次事故里长出来的。
本章对两类读者的收益(与前几章同款):前端读者——组件测试(第 9 章兑现)和 E2E 的等待策略,是前端测试的日常;后端读者——契约/越权/重放/事务边界测试,是后端测试的主战场;两类读者共同的收获:测试第一次有了完整的体系(金字塔+承诺映射+投入决策)——"怎么证明业务正确"从口号变成方法论。
本章与第 19 章的边界:本章测的是"业务逻辑的正确"(规则/契约/权限/防线);第 19 章安全章会讲"攻击视角的测试"(XSS/注入的验证、渗透测试的思路)——安全测试是安全章的内容,本章只提一句:安全漏洞也是"承诺"("用户数据不能被偷"是承诺),它同样需要测试——但测法不同(不是给正常输入断言输出,是给恶意输入断言"被挡住"),第 19 章展开。
本章小结
测试速查(第四部分回指本章时翻回这里):
- 测试第一问是"回归":改坏了怎么办——测试存在的第一理由是"改了还能跑"(功能回归+性能回归两层);
- 测试金字塔:单元(快/准/稳,测规则)→ 集成(真环境,测契约/权限)→ E2E(完整旅程,核心路径)——越下越多越快越便宜;
- 三层对应本书:单元=第 12 章业务层、集成=第 12 章链路+第 7 章契约+第 13 章权限、E2E=第 15 章联调清单;
- 第 17 章防线的测试:状态机转移表(单元)+ 重放测试(E2E)——交易系统额外测"重复/乱序/丢失";
- 测试与需求映射:第 4 章验收标准=测试用例——测试是承诺的代码形态;
- 投入决策:金字塔比例(单元 70/集成 20/E2E 10,九步第 11 次应用);测试代码也是负债;该测规则不测框架。 (六条速查对应本章结构:1 是"为什么测",2-4 是"怎么测",5 是"测什么",6 是"测多少"——第 19 章安全测试、第 22 章 CI 回指本章时重点翻 2(金字塔)和 6(投入决策)。)
测试的进化路径(第 2 章演进循环的又一轮):阶段 1 手工点一点(第 15 章联调清单)→ 阶段 3 分层测试(本章,被"改 A 崩 B"逼出)→ 第 22 章 CI(提交自动跑,被"人记得跑"逼出)——每一次演进都是"原方案无法承受"逼出来的:手工点一点在接口 12 个时崩了,人肉跑测试在团队 3 人时崩了——测试体系的每一步,都是第 2 章循环的实例。
第四部分进度:16 性能 ✅ 17 交易 ✅ 18 测试 ✅——还剩 19 安全、20 质量——第四部分的"稳",本章是第一块砖。
- 测试的第一问是"回归":改坏了怎么办——"改了还能跑"比"证明它对"更接近测试的本质;
- 测试金字塔:单元/集成/E2E——越往下越多越快越便宜,投入按金字塔分配;
- 测试是对业务正确的承诺:第 4 章验收标准、第 7 章契约、第 13 章权限、第 17 章防线——每个承诺都有对应的测试;
- 测试代码也是负债:该测规则、不测框架——写测试省的是未来返工的钱。
下一章
测试体系立起来了:改动了代码,几分钟内就能确认"没改坏"。但测试只能确认"我们以为对的是对的"——如果攻击者看到的东西和我们看到的不一样呢?
业务有了钱(第 17 章的交易),有了用户数据(注册信息、订单、地址)——树大招风:XSS、SQL 注入、CSRF、越权……第 13 章说过"信任模型必须从第一行代码就假设对方不怀好意",现在到了兑现的时刻。
第 19 章,Web 安全:攻击者利用的是业务逻辑的漏洞。