专业书籍精读 · 持续交付 · 第 4 章
Continuous Delivery · Ch 4 · Jez Humble & David Farley · 2010
你手机里的 App,每次更新前都得有人确认「它没坏」。问题是:要确认的东西成千上万,谁来确认、什么时候确认?《持续交付》第 4 章的答案不是「多测一点」,而是把「测」拆成几种完全不同的活,分给不同的人和不同的时机。
盖一栋楼,有三种检查。第一种:工人每砌一段墙,随手用水平尺量一下——几秒钟,而且一量就知道是哪块砖歪了。第二种:一层封顶后走一遍,试试水电通不通——慢一些,但能查出单块砖看不出来的问题。第三种:楼盖好了请个老验房师进去转两天,他会打开你根本没想到要打开的柜门,说「这个门框,人拎着行李根本转不过身」。
三种缺一不可,性价比却天差地别。可怕的是很多团队只有第三种——墙砌到十八层都没人拿水平尺量过,等验房师说「三楼开始歪了」,那已经是要拆的量级。
过去的默认干法:程序员写完扔给专门的测试组,最后几天集中人手,照着一张几百条的清单从头点一遍。三件事必然发生。一,反馈太晚——问题是三周前埋下的,写的人早忘了当时怎么想。二,成本只会涨——功能越加越多,清单越来越长,发布还想越来越快。三,做过的功课不能存档——这轮点完,下轮还得从头再点,谁也说不准漏了哪几条。
这一章把测试按两个问题重新分类:这个检查是想「帮团队别做错」,还是想「挑出做出来的东西哪儿不对」?它关心「业务上对不对」,还是「技术上稳不稳」?两两一组,正好四类活。
分完之后,最关键的那条分工线就自己浮出来了:每次都一样、答案只有对或错的检查,交给机器每天自动跑几千遍;要靠好奇心和经验去「找你根本没想到的问题」的,留给人。把这两者搞反,才是多数团队真正的病根:机器闲着,人却在一遍遍手工点那张清单。
机器负责的部分自己还要分层:又快又小的检查多做(几千个,几分钟跑完,一红就知道是哪块砖),慢而全的整体检查少而精(几十个,管的是「用户能不能把钱付出去」)——因为整体检查慢、贵,还常会无缘无故失败一次,堆多了就没人信它。
人这边正相反:把照着清单点屏幕的事全交出去,换来的时间用在「像个真实用户那样去乱逛,看能撞出什么」——机器只会检查你事先写下的那几条。
一句诚实的代价:自动化测试本身也是代码,要写、要养,写歪了会天天误报、拖慢所有人,最后被整队一起无视。
测试不是发布前的最后一道工序,而是从第一天起就并行的一堆不同的活。能重复的交给机器、多做小的少做大的;靠创造力的留给人——两者都要,但千万别互相顶替。
想进到四象限、分层比例、对比表和流水线落位图? → 切到精读版
你以为「测试策略」是决定测多少、谁来测、覆盖率定几成,其实这一章问的是另一个问题:每一类风险,该由谁在什么时候、用多大代价去回答?答案有两半。第一半是分类——用「面向业务 / 面向技术」×「支持开发过程 / 评判已有产品」两条轴把测试摊成四个象限,每格的问题与自动化命运都不同:能重复、答案二值的交给机器天天跑;靠好奇心去发现「你根本没想到的问题」的(探索性、易用性)留给人。第二半是落地顺序——新项目、中途项目、遗留系统三种起点,先补哪一类完全不同。全章立场一句话:测试不是一个阶段,是贯穿始终的活动;质量是全队的事,不是测试组的事。
Part I「基础」的收尾章,也是第 3 章欠下的债的偿还:持续集成要求「一套全面的自动化测试」,本章回答那套测试到底该长什么样、谁写、跑在哪。往后,第 5 章的部署流水线会把这四类测试排成先后关卡(第 7 章提交阶段跑单元测试、第 8 章展开自动化验收测试、第 9 章处理非功能测试)。今天的对应物:测试金字塔之争、Google 的 small / medium / large 测试分级、契约测试(contract testing)、以及「灰度 + 可观测性」这种把验证右移到生产的做法。
先摆个场景。30 人团队做支付网关,两周一版,测试排在发布前最后三天:6 名测试工程师照着一张 400 条的手工回归清单从头点一遍,一轮两天,紧张时就「挑重点跑」。三处结构性漏水:
不解决会怎样?项目末期长出一个工期不可测的「测试阶段」黑洞——和第 3 章那个「集成阶段」黑洞是同一种病:本该每天做一点的事,攒到最后一次性结算。但本章同样反对另一个极端:全押自动化也不行,机器只会验证你事先写下的断言,而真正贵的缺陷往往是没人想到要写断言的那些。所以解法不是「自动化一切」,而是先分类,再决定每一类的命运。
原书开宗明义:测试是一项跨职能(cross-functional)、贯穿全程的活动,质量由全队负责,而不是丢给下游的测试部门。落到动作上有三条:测试人员从第一天参与写验收条件,与分析师、开发一起把「怎样算做完」定死在开发之前;「完成」的定义里包含「测试通过」;缺陷一旦逃逸到后期或生产,先写一个能复现它的自动化测试再去修,否则同一个坑一定被踩第二次。
更根本的理由是这套测试网给的东西:改动的勇气。没有它,重构就是赌博,团队会自然选择「绕着走、复制一份改」。这与第 3 章互为因果:CI 让你天天集成,测试网让你敢天天集成。
本章的骨架是 Brian Marick 提出、后由 Crispin 与 Gregory 在《Agile Testing》中推广的测试象限。两条轴各问一个问题:这个测试面向业务(用领域语言写,客户看得懂)还是面向技术(只有开发看得懂)?它支持开发过程(动手之前 / 之中写,防止缺陷被引入)还是评判已有产品(做出来之后跑,去发现你没想到的问题)?
面向技术那一格是开发者的安全网:单元测试(几千个在几分钟内跑完,红了几乎能直接定位到函数)、组件 / 集成测试(连着真实依赖一起测,慢一个数量级,但能抓住单元测试看不见的接线问题)、部署测试(冒烟测试)——部署完立刻跑的一小把检查,只问「服务活着吗、配置连对了吗」,成本最低、收益最高,却最常被忘。
面向业务那一格是自动化功能验收测试:用业务语言写(「给定余额为 0,当用户下单,则被拒绝并提示充值」),在开发之前写好并当作验收条件,跑在尽量接近生产的环境上。它一身两职——既是「做完没有」的判据,也是此后永久的回归网。原书的态度很清醒:这是四类里最贵、最脆、最难维护的一类,要造数据、拉起整套环境、跨越界面。所以数量要克制(数十个而非数千个),只覆盖最有价值的业务路径,并分层写——业务意图一层、驱动应用的技术细节另一层,界面一改不至于全线飘红。
手工的那一格有三件事,价值都来自人。演示(showcase):迭代末把真实系统跑给客户看,暴露的是「做对了没有」——需求理解错了,多少自动化测试都是绿的。易用性测试:只有人能说出「这个流程我走不下去」。探索性测试:不是乱点,而是一项有目的的创造性活动——边学系统边设计新实验,专门去撞没人写过断言的角落。原书由此点明自动化的真正目的:不是取代测试人员,而是把他们从重复的手工回归里赎出来去做这三件事。
非功能那一格是容量、性能、安全、健壮性。它必须先把要求量化成可判定的阈值(如「峰值 3000 QPS 下 p95 响应时间不超过 200 毫秒」——p95 指把一百次请求按耗时排队、第 95 快的那次),否则没法自动判定通过与否,只能沦为一份没人读的报告。它还需要专门工具与接近生产规模的环境,故排在验收测试之后。
要让上面那些测试跑得快、结果稳,就不能每次都真去调第三方支付、真去发短信——外部依赖慢、不稳、有副作用还可能收费。测试替身就是顶替它们的假货,原书沿用 Gerard Meszaros 的分类:dummy(纯占位)、fake(能跑的简化实现,如内存数据库)、stub(对预设调用返回预设答案)、mock(在 stub 之上还断言「被调用了几次、参数是什么」)。
代价必须说清楚:mock 用得越多,测试越绑死实现细节——测的变成「代码怎么做的」而非「事情做成没有」,一次正当重构就让上百个测试同时变红,团队很快学会不再重构。原书的平衡点是:替身用来隔离,但必须另留一小撮真打外部系统的测试并定期跑,否则你只证明了「我的假设和我的假货自洽」。术语坑:原书的「集成测试」特指与外部系统对接,与今天常说的「集成测试=跨模块测试」不是一回事。
原书不给放之四海的清单,而是按项目状态给策略(详见下节表 3)。最该记住的是遗留系统那条路径:Feathers 的定义是「没有测试的代码就是遗留代码」,所以顺序应是——先把构建与部署自动化(第 2 章),再给即将改动的那一小块写表征测试(characterization test:把它现有的行为原样记录成测试,不管这行为对不对),然后才动手改。这里全局覆盖率是个陷阱:覆盖率是诊断信号,不是目标——一份 100% 覆盖却几乎不带断言的测试套件完全可能存在。
四象限最终要落到时间轴上:越靠前的关卡越快、越便宜、越该挡住大多数缺陷;越靠后的环境越像生产、覆盖越全、反馈越慢。
表 1 · 四象限速查:各象限的定位、自动化命运与「不做会怎样」
| 象限 | 典型测试 | 自动化? | 落在流水线哪一关 | 不做会怎样 |
|---|---|---|---|---|
| 面向技术 · 支持开发 | 单元、组件、部署 / 冒烟 | 必须自动化 | 提交阶段(≤10 分钟) | 没人敢重构,改一行要靠祈祷 |
| 面向业务 · 支持开发 | 功能验收测试 | 必须自动化 | 验收测试阶段 | 做出来的东西不是客户要的,且无回归网 |
| 面向业务 · 评判产品 | 探索性、易用性、演示 | 不能自动化 | 按需的手工阶段 | 只找得到你已经想到的问题 |
| 面向技术 · 评判产品 | 容量、性能、安全 | 自动化跑,人解读 | 非功能测试阶段 | 功能全对,一上量就塌 |
表 2 · 三层自动化测试的经济学:为什么形状必须是金字塔
| 单元测试 | 组件 / 集成测试 | 端到端验收测试 | |
|---|---|---|---|
| 单个耗时 | 毫秒级 | 秒级 | 数十秒到分钟级 |
| 红了能定位到 | 某个函数 | 某个模块边界 | 只知道「这条流程坏了」 |
| 片状风险 | 极低 | 中(依赖环境与数据) | 高:网络、时序、脏数据 |
| 维护成本 | 低,但会绑实现细节 | 中 | 最高:环境、测试数据、界面变更 |
| 能证明什么 | 这块代码按设计工作 | 模块接线正确 | 业务价值真的能交付 |
| 该有多少 | 数千个(绝大多数) | 数百个 | 数十个,只覆盖主路径 |
为什么不能倒过来?一个简单算术:200 个端到端测试、每个有 0.5% 概率随机失败,整套一次全绿的概率只剩 0.995²⁰⁰ ≈ 37%——三次里有两次会红,且多半不是真 bug。红灯一旦常态化,第 3 章那条「红了全队停线」的纪律当场作废。这就是「冰淇淋甜筒」反模式的致命处:它不是慢一点,是让整套测试失去可信度。
表 3 · 三种起点:先补什么、别做什么
| 起点 | 先做 | 别做 | 判据 |
|---|---|---|---|
| 新项目 | 第一天就 TDD + 验收条件先行 | 别说「先上线,测试以后补」 | 此时边际成本最低,之后只会更贵 |
| 中途项目 | 先给最常走的几条主路径写验收测试,哪怕只有 10 条 | 别停下开发去「补齐覆盖率」 | 先挡住「上线即崩」,再碰到哪补哪 |
| 遗留系统 | 先自动化构建与部署 → 给要改的那块写表征测试 | 别追求全局覆盖率,别大爆炸式重写 | 测试跟着改动走:动到哪,网织到哪 |
表 4 · 依赖怎么处理:四种替身与真实依赖
| 做法 | 它做什么 | 代价 / 风险 | 什么时候用 |
|---|---|---|---|
| 真实依赖 | 直接调真数据库 / 真第三方 | 慢、不稳、有副作用,可能收费 | 留一小撮定期跑,验证假设仍成立 |
| fake | 能跑的简化实现(内存数据库) | 与真实行为可能有微妙差异 | 组件测试的默认选择 |
| stub | 对预设调用返回预设答案 | 只固定输入输出,够用且稳 | 大多数场景的首选 |
| mock | 额外断言「被怎么调用的」 | 绑死实现细节,重构即全红 | 只在交互本身就是需求时(如「必须只扣一次款」) |
这一章是部署流水线的装料清单:第 5 章给关卡骨架,本章决定每一关里放什么。面试里问「你们的测试策略是什么」,好答案不是报覆盖率数字,而是本章这几条:各层测试的比例与耗时、验收测试由谁在什么时候写、片状测试怎么治、遗留模块怎么起步。
70% / 集成 20% / 端到端 10%——与本章的分层主张同调,但把比例说死了。M. Wacker「Just Say No to More End-to-End Tests」, Google Testing Blog 2015 ↗80% / 15% / 5%,并要求「永远写能解决问题的最小的那个测试」。Winters, Manshreck & Wright,《Software Engineering at Google》Ch.11 Testing Overview ↗60% 视为可接受、75% 值得称道、90% 为典范,同时强调覆盖率不能当作测试质量的代理指标、不该被当成打勾任务。Google Testing Blog「Code Coverage Best Practices」, 2020 ↗① 一句话:测试不是一个阶段,是贯穿全程的跨职能活动;质量归全队,不归下游的测试组。
② 骨架是四象限:面向业务 / 面向技术 × 支持开发过程 / 评判已有产品——每格的问题、写作时机与自动化命运都不同。
③ 分工线:能重复、答案二值的交给机器;靠人的好奇心的留给人——两者都要,不能互相顶替。
④ 自动化验收测试用业务语言在开发之前写,既是验收条件又是回归网;但它最贵最脆,要少而精、分层写。
⑤ 形状必须是金字塔:单元最多、端到端最少。200 个端到端测试各 0.5% 片状,整套全绿概率只剩约 37%——倒过来搭,红灯就失去意义。
⑥ 替身让测试快而独立,但 mock 越多越绑死实现;另留一小撮真打外部系统的测试。注意原书的「集成测试」=对接外部系统。
⑦ 起点决定顺序:新项目第一天做对;中途项目先给主路径写十条验收测试;遗留系统先自动化部署、再给要改的那块写表征测试——不追全局覆盖率。
⑧ 实证:Google 建议 70/20/10、《SWE at Google》给 80/15/5,Google 明确覆盖率不是质量代理,DORA 要求测试主要由开发者维护,Spotify 用蜂巢说明比例随架构而变。