速成课 · No. 21
test 不过是自动检查你代码的代码。人们以为它的职责是证明今天的代码能用——但真正的回报在后面:正是 test 让你明天能够修改、重构、添加功能,而不破坏已经正常工作的东西。学会这些种类和术语,测试就不再是苦差事,而变成那个让你跑得飞快的东西。
19 个概念 · 每个都有图 · 结尾有速查表
几乎所有人都误解了 test 的用途。它们看起来像一种证明代码能用的手段;但它们真正的价值更深刻、也更实用——看清这一点会改变你写它们的方式。
地图:七章里的 19 个概念
一张地铁图:先一眼看清所有线路,再一站一站地坐过去。
课程里的每个概念都属于七章之一。先讲测试为什么存在。然后是测试的种类,以及如何平衡它们。接着是隔离一段代码所需要的替身。再往后:写好一个 test 的手艺、如何信任整套测试、以及让它真正派上用场。
怎么读这门课
一本护照:每个人都是同样的栏目,所以很容易比较。
每个概念都按同样的方式讲:它的英文名称(记住它——文档和面试里遇到的就是这个术语)、一幅生活中的画面、一张图,以及一张带有这些栏目的卡片:问题、思路、例子、代价、何时使用,以及关联概念。有一个想法贯穿整门课:测试是对未来改动的保险,而不是代码曾经能用过一次的证书。
Automated Test — 检查代码的代码
另一个人在计算器上把你的算术重做一遍,并标记出任何对不上的——自动、不知疲倦,而且每次都一样。
test 是一小段代码,它用一个已知的输入来运行你的代码,并检查结果是否如你所料。「给定 2 和 3,add 会返回 5 吗?」如果是,test 就 pass;如果不是,它就 fail 并告诉你。这就是全部机制。它的威力在于自动且可重复——你可以在几秒内运行成千上万次这样的检查,每次改动任何东西时都跑一遍,而不需要人工逐一手动复核。
- 问题
- 手动检查又慢又无聊——所以每次改完之后没人会把所有东西重新检查一遍。
- 思路
- 把检查写成代码:已知输入 → 调用 → 与预期比较。结果:通过或失败。
- 例子
- 「
add(2, 3)必须返回 5」——一行代码,几毫秒就跑完。 - 代价
- 测试也是代码:要写、要读、要维护。
- 何时使用
- 用于任何你打算持续修改的代码。
- 关联
抓出 Regression;分为 Unit、Integration 和 E2E;在 CI Gate 中运行。
Safety Net — 真正的意义是无所畏惧地修改
攀岩者身上的安全带:它不替你攀岩,但让你敢于尝试大胆的动作,因为失手也不会致命。你爬得更猛,因为摔下来有人接住。
误解就在这里。测试主要不是为了证明今天的代码是正确的——而是为了明天。当你修改代码、重构它、或添加功能时,一套好的测试套件(test suite)会立刻告诉你,你是否破坏了以前能用的东西。这张安全网让你能够自信地修改庞大的代码库,而不是害怕碰任何东西。测试让你能继续飞快前进,而身后的东西不会悄悄坏掉。
Regression — 改动破坏了原本能用的东西
一排穿过整座房子的绊线:只要你根本没在看的某个房间有了动静,警报就响了——你不必亲自巡逻每个房间。
regression 指的是一次改动破坏了以前能用的东西——往往发生在离你改动处很远的地方。没有测试,regression 会一直藏着,直到用户发现它。有了测试,你的改动一旦破坏了旧行为,就会有一个 test 变红并指出它,就在当下。这就是日常的回报:你做一次改动,跑一遍测试,立刻知道自己是否也破坏了别处的东西。你在一个房间里干活,测试套件守着整座房子。
- 它是什么
- 一次改动破坏了以前能用的行为。
- 陷阱
- 它常常坏在离你编辑处很远的地方,在系统的另一个角落。
- 例子
- 你改了购物车的总价——结果发票开始显示错误的金额。
- 如何抓住它
- 针对旧行为的一套测试,每次改动都运行。
- 提示
- 发现了 bug?为它加一个 test,让它永远不会再回来。
- 关联
- 由 Automated Test 抓住;由 CI Gate 自动把关。
test 自动检查代码。它真正的价值不是一次性的证明——而是让你明天能修改,而不悄悄破坏今天还能用的东西。
测试的基础是 unit test:对一小段代码单独进行的、快速而专注的检查。你的大多数 test 都会是这种,而且理由充分。
Unit Test — 孤立地测试一小块
在用乐高积木搭建之前,先检查单块积木的形状和大小对不对——而不是把整座城堡搭完才发现有一块是错的。
unit test 检查一个小的单元——通常是一个函数——把它与系统其余部分隔离开。给它一个输入,检查它的输出,不牵扯别的东西。因为这一块又小又独立,test 失败时你确切知道什么坏了、坏在哪里。unit test 是测试领域的显微镜:窄、准,一次只对准一样东西。
- 问题
- 一个大检查失败时,你说不清是一百个部分里的哪一个出了错。
- 思路
- 一次只检查一个函数,与数据库、网络和其他一切隔开。
- 例子
calculateTax(100)返回20——没有数据库,没有 HTTP,几毫秒完成。- 代价
- 它看不到各部分如何协同工作——那由其他测试负责。
- 何时使用
- 用于你的大部分测试:业务逻辑、计算、规则、边界情况。
- 关联
Test Pyramid 的底座;由 Test Doubles 实现隔离;呈 Arrange–Act–Assert 的结构。
又快又多 — 这就是全部的思路
一个拼写检查器,眨眼间扫完整篇文档——快到你可以时时运行,而不是一个月才做一次的缓慢审阅。
因为每个 unit test 都很小,也不碰任何外部东西,所以它只需几毫秒——于是你可以拥有成千上万个,并在每次改动时都全部运行,不间断。这种速度就是关键:几秒钟能跑完的测试套件会被一直跑,在损坏发生的那一刻就抓住它。慢的套件会被跳过,被跳过的套件什么也保护不了。又快又多,正是 unit test 成为主力的原因。
Arrange–Act–Assert — 准备、执行、检查
一个简单的实验:布置好条件,做那唯一的动作,然后检查结果。准备清楚,一步动作,一次验证。
一个好的 unit test 有三个朴素的步骤——**Arrange–Act–Assert(AAA)**模式:准备好输入,对被测对象只调用一次,然后断言(assert)结果符合预期。assertion 就是检查本身——「答案必须等于 5。」让 test 保持这种清爽的结构,它就易读,失败也一目了然。一个难读的 test,没人会信任,也没人愿意维护。
- 问题
- 纠缠不清的 test:准备、调用和检查混在一起——看不出它到底在验证什么。
- 思路
- 三个块依次排列:准备 → 一次动作 → 检查(assertion)。
- 例子
Arrange:一个装有两件商品的购物车。Act:
cart.total()。Assert:总价等于 20。- 代价
- 几乎没有——只需要「每个 test 只做一个动作」的纪律。
- 何时使用
- 永远,在每个 test 里——unit、integration 还是 end-to-end 都一样。
- 关联
- 让 test 易读——从而成为 Living Documentation;Unit Test 的骨架。
unit test 孤立地检查一小块,只需几毫秒——所以你能时时运行成千上万个。准备输入,执行一次,断言结果。
unit test 单独检查各个部件,但软件是各部件协同工作。有两种更宽的 test 负责这一块——而三者之间恰当的平衡,有一个值得了解的形状。
Integration Test — 各部分协同工作
检查完每块乐高积木之后,你把几块拼在一起,确认它们真的能连上——积木单独看都没问题,但它们对得上吗?
integration test 检查多个部分一起是否正确工作——你的代码与数据库对话,两个服务交换数据。每个单元单独看可能都完美,却仍然连不上:格式不匹配,对另一端的假设有误。integration test 抓的正是这些接缝(seam)。它们比 unit test 慢,因为涉及更多真实运转的部件,但它们验证的是 unit test 按设计会略过的那些连接处。
- 问题
- 每个部分的 unit test 都通过了,可放在一起却不能工作。
- 思路
- 把几个真实的部分一起跑起来,检查它们之间的连接。
- 例子
- 一个 repository 把用户写入真实的测试数据库,再读回来。
- 代价
- 比 unit test 慢;需要搭好环境(数据库、服务)。
- 何时使用
- 用于接缝:代码 ↔ 数据库、服务 ↔ 服务、你的代码 ↔ 第三方 API。
- 关联
- Test Pyramid 的中间层;抓住 Over-mocking 所掩盖的问题。
End-to-End Test — 像用户一样走完整个系统
整出戏从头到尾、在真实舞台上的彩排——不是只查台词或道具,而是观众将看到的整场演出。
end-to-end test(E2E)像真实用户那样驱动整个系统——点按钮、填表单,检查整条技术栈上是否发生了正确的事。它是最贴近现实的 test,也是确认产品真正可用的最有价值的证明。但它同时也是最慢、最脆弱的:很多部分必须同时运行,界面上一个小改动就可能把它弄坏。你把 e2e test 用在少数关键流程上,而不是用在所有东西上。
- 问题
- 每个部分都测过了,但没人知道产品作为一个整体能不能用。
- 思路
- 沿着用户的路径走完整个真实技术栈,并检查结果。
- 例子
- 一个浏览器机器人:登录、把商品加入购物车、付款,看到「订单已提交」。
- 代价
- 最慢也最脆弱;界面上的小改动就会让它出错。
- 何时使用
- 用于少数关键流程:登录、支付、主流程。
- 关联
- Test Pyramid 的塔尖;太多的话就成了冰淇淋筒。
Test Pyramid — 恰当的平衡
金字塔之所以立得住,是因为底宽顶窄——把它倒过来立在尖上,它就倒了。形状本身就是稳定。
test pyramid 是关于平衡的经验法则:底部是很多快速的 unit test,中间是较少的 integration test,顶部是少数几个慢速的 end-to-end test。这种形状让你以低成本获得广泛而快速的覆盖,顶部再配上刚好够用的真实检查。反模式——大量慢速的 e2e test、很少的 unit test——叫冰淇淋筒(ice-cream cone),它又慢、又不稳、又痛苦。目标是金字塔,而不是冰淇淋筒。
- 问题
- 测试很多,但套件又慢又不稳定——于是大家不再运行它。
- 思路
- 底部是很多便宜快速的 test,顶部是少数昂贵缓慢的 test。
- 例子
- 成千上万个 unit test,几百个 integration test,针对主流程的十来个 e2e。
- 代价
- 你得有意识地决定每样东西由哪一层来检查。
- 反模式
- Ice-cream cone——倒过来的金字塔:很多 e2e,很少 unit。
- 关联
各层是 Unit、Integration、E2E;多出来的 e2e 会滋生 Flaky Tests。
unit test 单独检查各部件,integration test 检查它们一起工作,e2e test 像用户一样检查整个系统。守住金字塔:多而快,少而慢。
要孤立地测试一小块,你常常得替它所依赖的真实东西找个替身。这就是 mock 和 stub——它们既好用,也同样容易被滥用。
Test Double — 用替身代替真实依赖
用飞行模拟器代替真飞机:假的仪表和画面让你安全地练习那一项技能,而不必承担真正飞行的成本和风险。
要孤立地测试一小块,你会把它真实的依赖——数据库、支付服务、发邮件的组件——换成顶替它们的替身。任何这类替身的通称是 test double,就像电影里的替身演员。这样你就能在没有那个又慢、又贵、又不可预测的真东西的情况下测试你的代码。你不会希望 unit test 真的去刷一张信用卡或发一封邮件;你换上一个假装在做的替身,让 test 保持快速、可靠、自成一体。
- 问题
- 真实的依赖又慢、又花钱,或行为不可预测——用到它们的 test 很脆弱。
- 思路
- 换上一个行为像依赖、但只是假装的对象。
- 例子
- 用一个永远回答「支付成功」的替身来代替 Stripe。
- 代价
- 替身可能说谎:真东西的行为也许不一样。
- 何时使用
- 用于外部的、慢的、随机的东西:网络、支付、邮件、时间。
- 关联
两大类——Stub 和 Mock;让 Unit Test 得以隔离;用过头就是 Over-mocking。
Stub vs Mock — 罐头回答 vs 被检查的调用
stub 是一块纸板人像,到点说一句台词;mock 则是一位演员,还会向你汇报你对他说了什么、怎么说的。
两种替身常被混淆。stub 只是返回一个固定的、现成的回答——「假装数据库返回这个用户」——这样你就能测试代码如何处理这个输入。mock 还会验证交互——「检查我的代码是否恰好用这个地址调用了一次 sendEmail。」stub 给你的代码喂数据;mock 则盯着你的代码对它做了什么。需要提供数据时用 stub,需要断言某个副作用发生了时用 mock。
- 它是什么
- 一个返回预先准备好的回答的替身。
- 例子
getUser()永远返回{ name: 'Ann' }。- 检查什么
- 在这份数据下你代码的输出。
- 何时使用
- 当你的代码需要从依赖那里_拿到_某样东西时。
- 它是什么
- 一个会记录调用并让你验证它们的替身。
- 例子
- 检查
sendEmail是否恰好被调用了一次,且地址正确。 - 检查什么
- 调用本身:有没有发生、发生几次、带什么参数。
- 何时使用
- 当副作用很重要时:邮件发出去了,款项付出去了。
Over-mocking — 说谎的测试
排练一出戏时,其他所有演员都用纸板人像代替——你可以把自己的台词说得滴水不漏,却依然不知道真正的演员阵容能不能配合起来。
替身很强大,但过量就危险。如果你把所有东西都 mock 了,你的 test 通过的只是你对其他部分行为的假设——而不是它们实际的行为。test 是绿的,真实的集成却是坏的。这就是 over-mocking 反模式。所以,要 mock 那些慢的、外部的或不可预测的依赖,但别把你正想验证的东西本身也假掉。一个完全建立在 mock 之上的 test 可以通过,而真实系统却失败——这是最危险的那种绿。
- 症状
- test 是绿的,可同一个函数在生产环境里却失败。
- 原因
- 它周围的一切都是假的——test 只是在检查你对其他部分的假设。
- 例子
- 数据库的 mock 永远说「ok」,而真实的数据库因为字段写错而拒绝了这条查询。
- 对策
- 只 mock 外部的、慢的东西;用 Integration Test 检查接缝。
- 关联
- 对 Test Doubles 的滥用;像 Flaky Test 一样侵蚀信任。
mock 和 stub 顶替真实的依赖,让你能孤立地测试。stub 提供数据;mock 验证调用。过度 mock,test 通过了,现实却崩了。
除了 test 的种类之外,还有写好 test 的手艺——以及一项颠倒惯常顺序、改变你设计代码方式的纪律:测试驱动开发。
TDD — 先写 test
先画靶子再射箭,而不是之后——这样你瞄准的是一个明确的目标,而不是在箭落下的地方周围画个靶心。
测试驱动开发(TDD,test-driven development)颠倒了惯常的顺序:你在写代码之前先写 test。循环是红、绿、重构——先为你想要的东西写一个会失败的 test(红),再写刚好能让它通过的代码(绿),然后在 test 的守护下整理代码(重构)。先写 test 迫使你在动手前就明确什么叫成功,并保证代码是可测试的,因为它就是从 test 里诞生的。
- 问题
- test 总是「以后再写」——结果代码写出来很难测。
- 思路
- 先写一个失败的 test,再写最少的代码,然后整理。如此循环往复。
- 例子
test「空购物车 → 总价为 0」失败;你写出
total();它变绿;你再整理一下。- 代价
- 一开始不习惯、也更慢;任务本身还不清晰时尤其困难。
- 何时使用
- 用于规则明确的逻辑:计算、校验、修 bug。
- 关联
产出可测试的代码和 Living Documentation;重构依赖于 Test Behaviour。
Test Behaviour, Not Implementation — 检查做什么,而不是怎么做
评判厨师要看菜好不好吃,而不是看他用哪只手握刀——你在乎的是结果,而不是他怎么做出来的那些私人细节。
写出经久耐用的 test,最重要的习惯是:检查代码做了什么,而不是怎么做的——检查它的行为,而不是它的实现。测试 getDiscount 返回了正确的价格,而不是测试它是否按顺序调用了三个具体的内部辅助函数。绑定在结果上的 test 能挺过重构——你可以重写内部,它们照样通过。绑定在实现上的 test,每次你整理代码都会坏,这会让人不再信任它们、最终把它们删掉。测试行为,test 才一直有用。
- 问题
- 行为明明没变,test 却在每次重构时都坏掉。
- 思路
- 只检查可观察的结果,并通过公开接口来检查。
- 例子
- 检查
getDiscount返回了 90——而不是它内部调用了哪些辅助函数。 - 代价
- 有时只靠输入和输出,某个边界情况更难触达。
- 何时使用
- 永远,作为默认做法。
- 关联
- 让 TDD 里的重构成为可能;多余的 Mocks 常常把 test 绑死在实现上。
Living Documentation — 测试即文档
一套写得好的 test 读起来像一份承诺清单:「给定这个,它就做那个」——比任何关于代码应该如何表现的注释都清楚。
一套清晰的测试套件同时也是活文档(living documentation)。每个 test 都陈述一个承诺——「给定空购物车,总价为零」——而且因为 test 会被运行,这份文档永远不会像注释那样过时;行为一变,test 就会坏。所以写 test 要让人读得懂:名字清楚,每个只讲一个行为,arrange-act-assert 一目了然。新人应该能通过读 test 来了解代码做了什么。这种可读性和检查本身一样有价值。
- 问题
- 注释和 wiki 会说谎:代码变了,文字还停在旧的。
- 思路
- test 是写成代码的承诺。违背承诺——test 就变红。
- 例子
- 一个名为「空购物车——总价为零」的 test。读起来就像一份规格说明。
- 代价
- 花时间起清楚的名字,并让每个 test 只讲一个行为。
- 何时使用
- 永远——尤其是新人会读的代码。
- 关联
- 建立在 Arrange–Act–Assert 和 Test Behaviour 之上。
先写 test(red-green-refactor),测试行为而不是实现,并让它易读——这样 test 能挺过重构,还能充当不会过时的文档。
一个数字和一个麻烦,决定了一套测试是否真的值得信任。误读 coverage,或容忍不稳定,绿色的套件就不再意味着任何东西。
Code Coverage — 测了什么,而不是测得多好
一张显示巡逻队开过哪些街道的地图——有助于发现从未去过的街区,但开车路过一栋房子,并不意味着你检查过门锁。
code coverage 是测试期间被运行到的代码所占的百分比。它是个有用的向导,用来找出完全没被测试的部分——那些从没人巡逻过的街道。但高 coverage 并不意味着好 test:代码可能被一个没断言任何有意义内容的 test 执行过,拿到 100%,却几乎什么都没验证。用 coverage 去发现盲区,而不要把它当作质量的证明。一行代码被碰到,并不等于这一行被检查过。
- 它是什么
- 测试期间被运行到的行(或分支)所占的比例。
- 有什么用
- 显示盲区——从来没有任何 test 运行过的代码。
- 陷阱
- 它不衡量质量:一行代码可以被运行,却没有任何 assertion 去检查它。
- 例子
- 80% 的 coverage,但所有折扣逻辑都在没覆盖的那 20% 里——风险就在那儿。
- 何时使用
- 把它当作照出缺口的手电筒,而不是目标。
- 关联
- 通过 Goodhart's Law 变成陷阱;不能替代 Risk-based Testing。
Goodhart's Law — 别让 coverage 变成目标
按刷出的公路英里数付钱给工人,结果看着他们在空地上刷出又长又没用的线来凑数——指标上去了,路并没有更安全。
当一个数字变成目标,人们就会去优化这个数字,而不是它原本衡量的东西——这就是 Goodhart 定律(Goodhart's law)。要求「100% coverage」,你得到的是为了碰到代码行而写的 test,而不是为了抓 bug:什么都不断言的空壳 test,在玩弄这个指标。coverage 是用来找缺口的手电筒,不是用来最大化的奖杯。追求真正的信心——这些 test 真的能抓住损坏吗?——让 coverage 只是众多提示之一,永远不是目标本身。
- 要点
- 当一个衡量指标变成目标,它就不再是个好指标。
- 在测试中
一条「coverage 不到 100% 就不许合并」的规则,会催生不带 assertion 的 test——只为凑数字。
- 例子
- 一个 test 调用了函数却什么也没检查——coverage 上升,保护为零。
- 对策
- 问「这个 test 能抓住损坏吗?」,并把 coverage 当作提示。
- 关联
- Code Coverage 的主要陷阱。
Flaky Test — 比没有 test 还糟
一个毫无理由地随机乱响的火警:一周之内,所有人都彻底无视它,所以真起火的时候,没人动弹。
flaky test 是指在代码没变的情况下随机时过时不过的 test——通常源于时序、顺序或某个隐藏的依赖。flaky test 是毒药:当一个红色结果可能只是噪音时,人们就不再信任任何红色,真正的失败也会被当成「大概只是 flaky」而放过。test 必须是确定性的(deterministic)——同样的代码,每次同样的结果。要么修好 flaky test,要么删掉它;一套你不信任的套件什么也保护不了。
- 它是什么
- 一个时过时不过的 test,而代码并没有改动。
- 成因
- 时序(sleep、竞态)、test 的执行顺序、隐藏的共享依赖。
- 例子
- 一个 test 等一个服务 100 毫秒;在繁忙的 CI 机器上,它有时放弃得太早。
- 危害
- 它教会团队无视红色——于是真正的失败溜了过去。
- 对策
- 让 test 变得确定性——或者删掉它。永远别和 flaky test 共存。
- 关联
- 在 E2E 里最常见;摧毁对 CI Gate 的信任。
coverage 显示测了什么,而不是测得多好——别让它成为目标。而 flaky test 比没有还糟:一旦红色可能只是噪音,每个红色都会被无视。
测试靠的是判断力,不是仪式。技巧在于:测那些要紧的东西,比例恰当,并把它接入你发布的流程——让套件赢得信任,而不是沦为形式主义的杂活。
Risk-based Testing — 测有风险的部分,而不是所有东西
你会反复检查降落伞和刹车;你不会去给杯架做压力测试。精力要花在失败真正会痛的地方。
不是所有代码都值得同样程度的测试。risk-based testing 的意思是,把精力集中在 bug 代价高昂或很可能出现的地方:核心业务逻辑、金钱、安全、棘手的边界情况、经常变动的部分。琐碎、低风险的代码可以少测一些。给所有东西都追着写 test,会浪费精力,还会造出一个又大又慢、没人愿意运行的套件。目标是那些能抓住你真正会后悔的故障的 test,而不是追一个数字——覆盖要紧的东西,胜过覆盖所有东西。
- 问题
- 测试的时间很稀缺,想什么都覆盖只会把它摊得太薄。
- 思路
- 失败代价高或可能性大的地方多测;琐碎的地方少测。
- 例子
- 支付的计算和访问权限——测得密;getter 和简单的排版——测得极少。
- 代价
- 你得诚实地判断风险,而不是只测容易测的东西。
- 何时使用
- 每当你决定把测试时间花在哪里的时候。
- 关联
取代对 Code Coverage 的追逐;在逻辑上以 Unit Tests 的形式最密集。
CI Gate — 每次改动都自动运行测试
一道闸机,车票无效就不开——检查就内置在闸门里,而不是靠某个人记得去看一眼。
测试只有真正运行时才能保护你。把它们接入你的流水线(pipeline),让它们在每次改动时自动执行,失败就阻止合并——这就是部署课里讲过的 CI gate。正是这一点,把 test 从「也许会跑的东西」变成了「不会有坏代码被合并」的保证。「没有 test 就不算发布」是值得坚持的标准:把一个没经过测试的改动送上生产环境,是一场你本不必下的赌注。
- 问题
- test 是有的,但大家忘了运行——于是坏代码落进了 main。
- 思路
- CI 在每次改动时运行每一个 test,在它们变绿之前拒绝合并。
- 例子
- 一个 pull request:CI 跑了 test,有一个失败——合并按钮被禁用。
- 代价
- 要等 CI;flaky test 在这里伤害最大。
- 何时使用
- 用于任何一周改动超过一次的项目。
- 关联
运行整座 Test Pyramid;容不下 Flaky Tests;在用户之前抓住 Regressions。
它们如何连成一体
一支管弦乐队:每件乐器都重要,但音乐只有合在一起才会出现。
这门课里的概念是作为一个系统运作的。pyramid 决定套件的形状,TDD 把它写出来,test double 给 unit test 提供隔离,CI gate 在每次改动时把这一切跑一遍——由此得到主要的回报:无所畏惧地修改代码。与之作对的是 flaky test 和 over-mocking,它们削弱对绿色的信任。而 coverage 只是暗示缺口在哪里。
- 它能让你自信地修改吗——明天它能抓住 regression 吗?
- 它是金字塔的形状吗——很多 unit、一些 integration、少数 e2e?
- test 检查的是行为吗,而不是一重构就坏的内部实现?
- mock 用在真正的外部依赖上了吗,而不是把被测对象本身给假掉了?
- 它又快又确定吗——没有 flaky test 在侵蚀信任?
- 它在每次改动时都在 CI 里运行吗,并阻止有问题的合并?
- 你自信地修改代码,相信套件会抓住你弄坏的东西。
- 你的 test 构成一座金字塔——底部又快又多,顶部是少数贴近现实的。
- test 检查的是行为,能挺过重构,而不是每次整理代码都坏。
- 套件又快又确定,并且你会修好或删掉 flaky test。
- test 在每次改动时都在 CI 里运行,并且你把 coverage 当作提示,而不是目标。
速查表:全部 19 个概念
我们为什么测试
- Automated Testtest
检查代码的代码:输入 → 调用 → assertion → 通过或失败。
- Regressionregression bug
一次改动破坏了以前能用的东西,往往离编辑处很远。
Unit tests
- Unit Test模块测试
孤立地测一个函数,几毫秒。你有成千上万个。
- Arrange–Act–AssertAAA
准备,做一件事,检查结果。
Integration、e2e 与 pyramid
- Integration Test集成检查
几个真实的部分一起跑——检查接缝。
- End-to-End TestE2E
用户眼中的整个系统。贴近现实,但又慢又脆弱。
- Test Pyramid测试金字塔
很多 unit,较少 integration,几个 e2e。别搞成冰淇淋筒。
替身
- Test Double替身,fake
对又慢、又贵或随机的依赖的替代品。
- Stub罐头回答,canned answer
返回预先准备的回答——给你的代码喂数据。
- Mock调用记录器
记录调用——验证某个副作用发生了。
- Over-mocking过度 mock
全部都 mock——test 是绿的,而真实系统在失败。
TDD 与好的 test
- TDDred-green-refactor
先写一个失败的 test,再写最少的代码,然后整理。
- Test Behaviour, Not Implementation黑盒测试,black-box testing
通过公开接口检查结果——test 能挺过重构。
- Living Documentation可执行的规格说明
test 是写成代码的承诺,永远不会过时。
信任这套测试
- Code Coverage测试覆盖率,test coverage
测试中运行到了多少代码。照出缺口的手电筒,而不是质量的衡量标准。
- Goodhart's Law指标 vs 目标
变成目标的指标就不再衡量了。100% coverage → 空壳 test。
- Flaky Test不稳定的测试
随机失败。对策:让它确定,或者删掉它。
用好它
- Risk-based Testing按风险测试
失败代价高或可能性大的地方多测。
- CI Gate合并检查
每次改动都跑测试;红——合并被阻止。
好的测试靠判断力:测有风险的部分,守住金字塔,检查行为而不是实现,并让这一切自动运行——这样绿色的套件才真正意味着可以放心修改。