XML之父:不对代码做测试就像“上完厕所不洗手”

作者|Tim Bray

翻译|王强

审校|燕珊

策划|Tina

XML 之父 Tim Bray 在日前发表的一篇博文中,基于自身的多年软件开发经验,分享了关于软件测试的那些事。“我非常确信,在我有生之年,对软件发展的最大贡献不是来自面向对象方法和高级语言、函数式编程、强类型、MVC 或其他任何东西,而是来自测试文化的兴起。”

成熟的软件开发人员非常清楚测试的重要性。但是从我的经验来看,很多人在这方面做得还不够。所以我写下这篇文章的目的是要警醒行业,或许这对于我们的从业人员来说本是多此一举,但现实显然不是这样。

本文的灵感来自于 Justin Searls 的两个 Twitter 帖子,这里引据其中的几句话:“你听到的几乎所有关于软件测试的建议都是糟糕的。这些建议要么看上去就很糟糕,要么会导致糟糕的结果,要么会让你专注于错误的事情(通常是工具),结果分散了注意力”,“几乎没有团队会编写富有表现力的测试、建立清晰的界限、快速可靠地运行,并且只会因有用的原因失败。大家的重点都错了。”(注意:Justin 显然身处测试行业。)

Twitter 帖子都弯弯绕绕、很难摸清脉络,所以我从中贴两张截图出来。

我先亮明我的观点:我认为这些不规则的所谓的“结构图”是大错特错,而且影响严重。

背景

自 1979 年以来,我一直从事软件开发工作。虽然我的观点很可能是错的,但这并不是因为我缺乏经验。我做过的几乎所有有意义的工作都是底层基础设施的东西:解析器、消息路由器、数据可视化框架、网络爬虫、全文搜索。所以如果你不在基础设施领域,我的一些发现可能就不那么有说服力了。

在我编程生涯的前 20 年,比如说直到 2000 年,行业内几乎没有软件测试的位置。一个后果是,如同 Gerald Weinberg 经常被引用的一句话:“如果建筑师按照程序员编写程序的方式建造建筑物,那么飞来的第一只啄木鸟就会摧毁整个文明。”

那时,对于自己写的任何软件,我总会在几年后开始讨厌它,因为它变得越来越脆弱和可怕。现在回头想想,我抗拒的大概是那些未经测试的代码给人带来的体验,因为经常会有一些小更改由于难以理解的原因意外而引发“大灾难”。

2000 至 2010 年的某个时候,情况开始发生变化。我的看法是,最初的推动力或多或少来自 Ruby 社区,并随着 Rails 的兴起而加速。我开始听到“测试的感染”(test-infected)这个词,我注意到如果提交的代码没有像样的单元测试,它们很容易被无情拒绝。

其他人告诉我,他们最初被围绕 Martin Fowler 的《重构》一书中的讨论打动,这本书最早出版于 1999 年,告诉大家你无法真正重构未经测试的代码。

特别是我记得自己在 2010 年参加了苏格兰 Ruby 技术大会,会上似乎有一半的演讲是关于测试最佳实践和技术的。我在那里学到了很多我今天仍在应用的知识。

我非常确信,在我有生之年,对软件发展的最大贡献不是来自面向对象方法和高级语言、函数式编程、强类型、MVC 或其他任何东西而是来自测试文化的兴起

我的信念

我们现在做事的方式好得多了。用回前面建筑师和程序员的比喻来说,文明不用再害怕啄木鸟了。

例如:我在谷歌和 AWS 工作的那些年里,我们遇到过很多中断和故障,但很少是因为某个软件错误这样简单的原因造成的。拙劣的部署、造成障碍的错误配置、证书问题(真是让人头疼)、DNS 小问题、实习生使用 Python 脚本做负载测试、金丝雀故障……痛苦的记忆来自很多因素,但往往不会仅因为一个小错误。

我不记得我是什么时候被“感染”的,但我可以保证,一旦你被感染,你永远不会容忍未经测试的代码了。

是的,你可以在上过公共厕所后不洗手;是的,你可以用手指吃意大利面,但负责任的成年人不会做这些事情,他们也不会交付未经测试的代码。顺便说一句,我后来也不再讨厌我开发了一段时间的软件了。

随着时间的流逝,我对糟糕测试的容忍度越来越低。我阻止别人升职、给别人打低分、斥责高级开发经理,而且一般都没得商量。我可以不树敌,可以容忍(大多数)情况,因为我尊重他人、待人友善和富有同情心。但在这个问题上我不会后退。

所以,我至死都会坚持这项原则(呃,我想应该是一系列原则):

  1. 单元测试是对软件未来的一项必不可少的投资。

  2. 测试覆盖率数据很有用,你应该密切关注它。

  3. 未经测试的老旧代码库可以而且应该逐步改进

  4. 单元测试需要使用单组 IDE 组合键非常快速地运行,并且完全可以像打寒战一样每隔几秒钟运行一次。

  5. 测试教毫无意义;只做有用的事情。

  6. 单元测试赋予代码审查者权力。

  7. 集成测试非常重要且非常困难,尤其是在微服务环境中。

  8. 集成测试需要 100% 通过,有失败被忽略是不行的。

  9. 集成测试需要运行得“足够快”。

  10. 加入基准测试对测试很有好处。

现在我将扩展上述列表中的声明。其中一些不需要进一步的拓展(例如“单元测试应该运行得很快”)。

但首先...... 你能证明测试的有效性吗?

哦,不能。我四处寻找有关测试效果的高质量研究,但没有找到什么结果。这并不令人惊讶。因为你需要找到两个强大的团队来执行重要的开发任务,并且在规模、结构、工具、技能水平和工作实践——在除测试之外的所有方面的表现都大致相同。然后,还需要在十年或更长的周期内研究他们的生产力和质量差异。据我所知,从来没有人这样做过,对这一结论我是很有信心。所以我们只剩下了经验积累,Nero Wolfe 称之为“经验总结出来的智慧”。

单元测试,现在和以后都很重要

当你创建一个新特性并实现一系列函数来完成它时,不要自欺欺人地认为你足够聪明,提前知道哪些东西容易出错,哪些将成为瓶颈,哪些将是你的继任者难以理解的。毕竟没有人足够聪明!因此,只要不是单行代码的内容都要编写测试。

上面那张 Spotify 的图中“实现细节”的标签是反对单元测试的,这让我很不爽。我在这里嗅到了不接地气的架构师的味道,这些人认为所有的工作就是在白板上正确地放置方框和箭头,而不是亲手去写分号和 if 语句。如果你的基础微服务代码没有经过充分测试,那么你就是在聚沙成塔而已。

在经过良好单元测试的代码库中工作会给开发人员带来勇气。如果重新实现一两个 API 会带来一些小的改变,那么你可以大胆一点去做。因为有了良好的单元测试,即使你搞砸了,你也会很快就发现问题。

请记住,代码的读取和更新频率高于编写频率。我个人认为,好的测试在第一次开发过程中就可以帮上开发人员的忙,并且不会减慢他们的速度。就我对这一职业的了解,单元测试为将要学习和修改这些代码的后续开发人员们,带来了显著的生产力提升并减轻了他们的痛苦。这就是业务价值所在!

那我们是否可以在哪里放宽单元测试覆盖率呢?比如早在 2012 年,我就写过关于测试 UI 代码的文章,尤其是关于移动 UI 代码。给它们编写测试太难了,在某些情况下可能不是一项好的投资。

另一个例子是 Java 世界专属的,存在依赖注入框架的情况下,你会得到非常大的文件,其中包含数以千计的配置乱码[*cough*SpringBoot*cough*],生命有限,实在没时间研究它们。

还有一些非常罕见的异常处理场景,你的数据中心很可能在遇到它们之前就陷入了困境,此时 IOException 会是你手头的那堆麻烦里最不起眼的,所以,也许我们不应该沉迷于那些 if err != nil 子句。

覆盖率数据

我并不会强求代码库应该达到某个覆盖率。但是这些数据其实很有用,你应该注意下它。

首先,找到特殊情况——覆盖率明显过低(或高)的文件,然后查找签入之间的更改。

覆盖率数据不仅仅是一个百分比数字。当我基本完成某段代码时,我喜欢运行一个测试,开启覆盖,然后快速浏览所有重要的代码块,查看绿色和红色的侧栏。每次这样做我都会得到惊喜:往往在有些文件上我本以为我的单元测试很聪明,但覆盖率实际上差了很多。这不仅让我想要改进测试,它还教会了我一些我原不知道的关于我的代码如何对输入做出反应的知识。

话虽如此,我非常尊重一些软件团队,他们有严格的覆盖率要求并能坚持下去。AWS 有一个团队,在他们的 CI/CD 管道中实际上有一个 100% 覆盖率的 blocking 检查。我不确定这是否合理,但这些人正在基础设施的关键部分编写非常底层的代码,在这种情况下不合理的东西可能也是合理的。而且他们比我聪明。

另外,我经历过的所有团队工作,都会遇到一些测试不足、拖累工作的遗留代码。即使是像我这样的测试狂也不会要求别人将高覆盖率单元测试改装到那些破东西上。

我见过一项很成功的策略,它有两个部分:首先,当你对没有单元测试的函数进行任何重大更改时,请编写单元测试。其次,导致覆盖率下降的签入是不允许的。

这很有效,因为当你在应对大型老旧代码库时,更新通常不会均匀地分散在其中,代码库中会有一些有用行为聚集的热点。所以如果你应用这个策略,代码“热区”的测试覆盖率会有机增长,达到相当不错的水平,而其他代码可能多年没有人接触或查看过,它们被忽略掉也没关系。

“不要宗教”

测试应该是最务实的活动,没有意识形态介入的余地。

请不要跟我扯什么 mock、stub、fake,没人在乎。在一个相关主题上,当我发现很多人在针对 DynamoDB 运行代码的单元测试中使用 DynamoDB Local 时,我感到非常惊讶。但它真的很有效,速度很快,而且比编写另一个 mock 或设置一个到实际云服务的链接要省事很多。不要教条主义!

然后来谈 TDD/BDD 信仰。有时,对于某些人来说,它很有用,给他们带来了更多力量。但对我来说它的纯粹形式从来没什么用,因为我的编码风格在早期阶段往往是混乱的,我一直在不断地重构和重构函数。如果我在开始编写它们之前就知道我想让它们做什么,那么 TDD 可能是有意义的。另一方面,当我已经草拟了一组我认为合理的方法,并且正在为基本代码编写测试时,我会提前准备并为还没写出来的代码编写更多测试。我这个样子没法成为 TDD“教会成员”,但我不在乎。

还有另一种信仰:在 Java 中对私有方法进行单元测试并不容易,Java 错了。有些人声称你不应该测试这些方法,因为它们不是类合约的一部分,那些人也错了。为了方便测试,妥协封装并让方法非私有是完全合理的。或者出于同样的原因应该编写 API 来获取接口,而非类对象。

当你针对复杂的 API 运行大量测试时,很容易就可以编写一个 runTest() 帮助程序,将正确布置参数并针对结果运行标准化检查。如果你不这样做,你最终会得到很多重复的剪切粘贴代码。

这里有争论的余地,没有教条的空间,我通常对此不大同意,因为当我更改了某些东西、并且是我以前从未见过的单元测试失败时,我不想在弄清楚发生了什么之前还得去搞清楚一堆帮助程序。

无论如何,如果你的工程师正在编写带有有效测试的代码,请不要跟他们讲任何废话。

审查人是你的朋友

有一次一个同事来找我寻求帮助,查看之后发现他们的问题很棘手。然后我让他们向我展示代码库,我提出了一些审查请求。

我看的前几段代码没有单元测试,但确实有注释说“稍后进行单元测试”。我走进他们的团队房间说:“伙计们,我们现在需要谈谈。”

在这里我想强调的是:单元测试一旦推后,就不会有人做了!

以及重点是,代码审查的目的不是正确性检查。审查人有权假设代码是有效的。审查人应该检查 O(N3) 瓶颈、可读性问题、笨拙的函数参数、不稳定的错误处理等。如果你没有足够的测试来证明你的代码的基本正确性,那么要求审查人考虑这些事情是不公平的。

进一步地说,我在审查时经常会遇到这样的情况,我很难弄清楚开发人员到底想在某段代码中完成什么。但首先,我会转到单元测试并查看它在做什么,因为有时从这里就可以明显看出开发人员所设想的函数用途。这也适用于需要修改代码的后续开发人员。

集成测试

做出本文前面所展示的图片的人似乎都认为这很重要,当然他们是对的。不过,我不确定“集成”和“端到端”之间的区别是否那么重要。

问题是我们在从单体迁移到微服务,于是这些测试变得更加重要,但也让它们更难构建。如果可以,这是坚持使用简单的单体应用的另一个很好的理由。

这反过来意味着你必须确保为你的集成测试规划时间,包括设计和维护时间。(单元测试只是基本编程预算的一部分。)

我知道这些测试很难写,我曾与其他优秀的团队一起工作,但他们的集成测试都很糟糕。

不好的一面是它们需要跑几个小时,这个就没什么好说的,因为时间目标经常没法达成。我们这么说吧:集成测试不需要像单元测试一样快,但它们确实需要足够快,这样你就可以在去上厕所或喝咖啡,或被聊天窗口打断时运行它们了。不过这还是很难实现的目标。

最后,我一次又一次地看到集成测试日志显示很多失败,一些开发人员会说“哦,是的,那些测试是不稳定的,它们有时会失败。”出于某种原因,他们认为这是可以的。要么测试执行了一些可能在生产中失败的任务,在这种情况下你应该将失败视为 blocker,或者这些任务不会在生产中复现,在这种情况下你应该将它们从该死的测试套件中取出,然后测试就会运行得更快了。

基准测试

因为我总是在处理对性能非常敏感的代码,所以我经常会编写基准测试,一段时间后我养成了将其中一些留在测试套件中的习惯。因为我已经观察到很多由性能下降引起的中断,比如某个配置改动将 TLS 计算从硬件推入 Java 字节码这样的蠢事。你真的会希望提前发现这种情况。

工具链

可用的工具有很多,足够用了。让你的团队就他们将要使用的内容达成一致,并成为相应的专家,然后不要把你的问题归咎于工具上。

我们的处境

我认为我们的整体情况还不错,因为大多数理智的组织都开始表现出相当好的测试纪律,尤其是在服务端代码方面。就像我说的,我在生产代码中看到的错误比以前少了很多。

而且每个团队都必须与那些可怕的、未经检验的、停滞不前的遗留代码池作斗争。打起精神来吧,处理它们只是工作的一部分。而且不管怎样,你可能也写过那样的代码。

但每天总有团队会迷失方向,“开始在上完厕所后不洗手”。不要这样做,并且不要发布未经测试的代码。

关于作者

Tim Bray,全名 Timothy William Bray,有“XML 之父”之称,XML 和 Atom 标准的创建者,曾先后就职于 DEC、Sun、Google 等公司。

原文链接:

https://www.tbray.org/ongoing/When/202x/2021/05/15/Testing-in-2021

今日好文推荐
为什么软件工程师出身的CEO越来越“香”?
70万行代码、历时20年,一名开发人员写出的史诗般的计算机程序
被疫情加速的通信云企业,“慢”下来后该走向何方?
大厂纷纷取消“大小周”,“反内卷”时代要来了?

活动推荐

近期,主打可视化开发的低代码平台再度翻红,有人认为通过低代码平台能让企业降本增效;另一种声音则直指低代码是概念炒作。当企业的数字化转型成为大势所趋,开发压力和人才短缺便是摆在他们面前的双重难题。

对于多数企业而言,他们都处在想用而不敢用的阶段。那么,低代码平台到底能不能用?企业又是否能通过低代码平台来缓解开发压力呢?8 月 11 日 20:00-21:00,InfoQ 技术公开课邀请了西门子 Mendix 高级架构师阮铭,他将给从业者讲清楚那些与低代码相关的真真假假,赶紧扫码预约抢座吧~

(0)

相关推荐

  • 如何培养软件开发人员的测试能力?

    谷歌公司实行开发测试混合,让开发人员在实现软件功能的同时负责单元测试和集成测试,这种做法提高了软件开发效率,但同时也要求开发人员具备一定的测试能力. 因为如果开发人员没有测试能力,那么单元测试和集成测 ...

  • PHPUnit 手册【笔记】

    一.安装composer require --dev phpunit/phpunit ^6.5 composer require --dev phpunit/dbunit 二.编写测试 A.PHPUn ...

  • 代码质量保证-单元测试框架pytest

    时间主题4.1(周三)20:00单元测试框架-pytest 在这周三的测试运维试听课程中,芒果给大家介绍了一个非常好用的python单元测试框架--pytest,这里我们来做个小总结.单元测试介绍单元 ...

  • 上完厕所后,卫生间的门是打开好还是关着好?很多人都做错了

    根据世界厕所组织数据显示,全球约有24亿人生活在没有厕所或厕所卫生较差的环境中,有的国家因为厕所卫生问题引发大量疾病,直接影响了国家生产力,造成的GDP损失高达5%. 卫生间是我们日常进出较多的地方, ...

  • 碰撞测试能信吗?顶级豪车为什么从来不做测试?

    我们知道,如今的汽车设计,越来越讲究碰撞安全.世界各国的碰撞检测机构不断提出新的测试项目,汽车厂家设计车辆的难度越来越大--比如25%偏置撞击试验就让很多名气很大的车折了戟. 于是乎身边的小伙伴突然想 ...

  • 美国儿科协会:孩子聪不聪明,不用做测试,智力的秘密都藏在手里

    孩子的智力发育一直以来都是中国父母的关注重点,毕竟没有什么是比孩子聪明更让父母感到骄傲的事情了. 可智力这玩意不是说提高就能提高的.为了让孩子获得智力开发的先机,中国父母可以说没少在这方面" ...

  • 去外包做测试工程师真的好吗?从业三年的过来人给你讲讲经验

    最近收到不少刚毕业的小朋友私信,说马上面临就业,在找工作的过程中,有不少软件测试的外包公司给我打电话让去面试,究竟要不要去?作为一名已经从业软件测试三年的"老人"而言这里特地开一期 ...

  • 做测试掌握税法小知识!

    哈喽小伙伴们 又到了每周小测试的时间! 这些税法知识你都掌握了吗? 第一题 判断题 点击空白处查看答案 纳税人在开具红字增值税电子专用发票时,无需追回已经开具的蓝字增值税电子专用发票. A:正确 B: ...

  • 为什么期货高手做交易从来不对行情做预测?

    所有交易系统的目的都是为了满足稳定盈利的主观诉求.而构建客观化.系统化的交易体系,其出发点又是主观混沌意识的觉醒.所以任何交易行为都不可能完全的脱离主观,所有客观的手段,也都要依赖于主观的逻辑和理念. ...

  • 做个题看看你适不适合做测试!

    其实看到这个题目已经好几次了,突然发现还是有很多人做错,于是又来考考大家把. 这里涉及到不少物理知识,但是作对的部分却和物理无关! 如果上来选择4! 那么恭喜你,你被套路了!因为不通! 这下应该注意了 ...

  • 测开分享会第二期(做测试怎么玩TestOps-下)

    本次内容由清风为大家整理,整理内容仅仅是云层在微信群分享的知识,关于群里小伙伴们讨论的内容,以及云层的答复则没有整理.如果想要听到更多精彩的内容,与大家一起参与讨论测试的问题,那么就快加入我们吧! 由 ...

  • 测开分享会第二期(做测试怎么玩TestOps-上)

    本次内容由清风为大家整理,整理内容仅仅是云层在微信群分享的知识,关于群里小伙伴们讨论的内容,以及云层的答复则没有整理.如果想要听到更多精彩的内容,与大家一起参与讨论测试的问题,那么就快加入我们吧! 由 ...