第11章 测试概览
Adam Bender 撰写
Tom Manshreck 编辑
测试工作一直是编程工作的一部分。事实上,当第一次写计算机程序的时候,你几乎肯定会给程序扔一些数据,看看执行结果是否符合预期。很长时间以来,软件测试的工作方法形成了一种非常相似的过程,很大程度是手工完成,很容易出错。然而,自从2000年左右,软件行业的测试方法有巨大的提升,能够应对现代软件系统的规模和复杂度。这种演化的关键,就是开发者驱动的、自动化的测试实践。
自动化测试可以防止bug逃逸影响到你的用户。Bug在开发阶段发现的越晚,代价就越高,在很多情况下是指数级的。但是,“捉bug”只是动机的一部分。另一个同等重要的原因是支持变化的能力。无论是增加新的特性,还是针对代码健康度的重构,或者是完成一个更大的重新设计,自动化测试都能快速捕获错误,这样就会使变更软件更有信心。
迭代更快的公司能更快适应变化的技术、市场条件和客户口味。如果你有一个靠谱的测试实践,你就不用担心变化——你可以拥抱变化,把它作为开发软件的核心品质。你想更多更快地改变你的系统,你越需要一个更快的方法进行测试。
写测试的行为也会改善系统的设计。作为你自己代码的第一批客户,测试可以告诉你很多关于测试选择的事情。你的系统是不是和数据库过度耦合了?API支持需要的用例吗?你的系统处理了所有的边界用例吗?写自动化的测试强迫你在开发周期早期面对这些问题。这样做通常会获得更加模块化的软件,并为未来赋予更大的灵活性。
在测试软件的主题上我们已经颇费笔墨,这也是有原因的:对于这样重要的实践,对于很多人来说,想把它做好却是一项难以理解的手艺。在谷歌,虽然我们已经做了这么久,我们在把流程稳步地扩展到整个公司的时候,依然面临困难的问题。在本章中,我们分享一下我们帮助推进对话的过程中学到的东西。
我们为什么要写测试?
为了更好地理解如何发挥测试工作的最大功效,让我们从头说起。当我们谈论自动化测试的时候,我们到底在谈论什么?
最简单的测试定义由如下条目定义:
- 你正在测试的一个简单的行为,通常是一个你调用的方法或API
- 你传入这个API的一个具体的输入,一些值
- 一个可以观察的输出或行为
- 一个可控的环境,例如一个独立隔离的进程
当你执行一个这样的测试的时候,把输入传入到系统,验证输出,你就会知道系统行为是否符合预期。当放在一起的时候,成百上千简单的测试(通常叫做一个测试集(test suite))会告诉你整体的产品是符合设计预期的,并且更重要的是,它也能告诉你什么时候不是。
创建和维护健康的测试集需要很多努力。随着代码库增长,测试集也会增长。测试集会开始面临类似不稳定和缓慢的挑战。这些挑战如果不能良好的解决,那么这个测试集就会严重受损。一定要明白,测试的价值来源于工程师对于它们的信任程度。如果测试工作成了生产力洼地,常常带来劳累和不确定性,工程师就会失去信任,转而去找变通方法。一个差的测试集会比彻底没有测试集更糟糕。
除了助力公司快速构建很棒的产品之外,测试工作在确保生活中重要的产品和服务安全性方面正在变得至为重要。如今,软件更深度的融入到我们的生活之中,软件缺陷不仅仅是导致一点点麻烦那么简单,它们会耗费大量的金钱,导致资产的损失,甚至最严重的付出生命的代价。
在谷歌,我们已经下定决心:测试工作不能是马后炮。聚焦质量,测试是我们如何完成工作的一部分。我们知道,有时是痛苦的认识到,如果不把质量作为我们的产品和服务的一部分,将会不可避免地导致糟糕的结果。因此,我们把测试工作作为我们工程师文化的核心部分。
Google Web Server的故事
在谷歌早期,大家一般认为工程师驱动的测试工作不太重要。团队通常依赖聪明的家伙把软件做好。很少部分系统运行大的集成测试,大部分都是裸奔。有一个产品似乎最难受:这个产品叫做Google Web Server(谷歌网页服务器),也称作GWS。
GWS是服务于谷歌搜索查询的网页服务器,它对于谷歌搜索的重要性不亚于机场的空中交通管制。时间回到2005年,随着项目规模和复杂度的膨胀,生产率急剧下降。版本的bug越来越多,也需要越来越长的时间修复。团队成员变更服务时信心不足,经常是在生产环境功能不可用的时候才发现出了错。(那段时间,超过80%的生产推送包含影响用户的bug,而不得不回滚。)
为了解决这些问题,GWS的技术负责人决定制定一个工程师驱动的、自动化测试的机制。作为这个机制的一部分,所有新的代码变更都要求包含测试,并且测试会持续运行。在引入这个机制一年之内,紧急修复的数量下降了一半,即使这个项目每个季度都有创纪录的变更发生。即便面对空前的增长和变化,引入的测试在谷歌为最为关键的项目之一注入了生产力和信心。今天,GWS有数万个测试,每天几乎都发布,很少有客户可见的故障。
GWS的变化标志了谷歌测试文化的分水岭,因为公司其他部门的团队看到了测试的好处,也行动起来采取相似的策略。
GWS经验给到我们最关键的洞察之一是你不能只依赖语法能力来避免产品缺陷。即使每个工程师只是偶尔写出bug,在一个项目上你有了足够的人手之后,你就会被不断增长的bug列表淹没。假设有100人的团队,工程师非常优秀,每个月只产生一个bug。总共来讲,这组优秀的工程师每天还是能产生5个bug。更糟糕的是,在一个复杂系统,当工程师改造已知的bug,在附近写程序的时候,修复一个bug经常导致另外一个bug。
最好的团队会找到方法,把成员的集体智慧转换成整个团队的好处。那就是自动化测试工作完成的事。团队的一个工程师写完一个测试之后,就添加到一个其他人也能使用的公共资源池里。团队其他人现在就能运行测试并且在它能发现问题的时候能够受益。可以和基于调试的方法比较一下,这种方法每次一个bug出现的时候,工程师必须付出使用调试器深入调试的代价。这种工程师资源的代价是夜以继日的,这也是GWS反其道而行之的根本原因。
以现代化开发的速度进行测试
软件系统正在变得越来越大,并且前所未有的复杂。谷歌一个典型的应用或服务一般都有数万行代码。这个应用或服务会使用数以百计的库或者框架,并且需要通过不可靠的网络被交付到不断增长的各种平台之上,平台上有不可计数的配置确保能够运行。更加糟糕的是,新的版本会频繁地推送给用户,有时是每天很多次。每年一次到两次的更新,软件非常瘦身的时代已经过去了。
人类手动地验证系统的每一个行为的能力,已经不能更上大部分软件中需求和平台爆发的节奏。想象一下全部手工测试谷歌搜索的所有功能吧,比如查询航班,电影时刻表,相关的图片,以及网页查询结果(见图11-1,图片略)。即使你明白怎么解决这个问题,但是你依然需要把这个工作量乘以谷歌搜索支持的语言、国家和设备,并且别忘了检查可访问性和安全性。尝试让人们手动和每个不会扩展的功能交互,进而评估产品质量。当说到测试的时候,有一个清晰的答案:自动化。
编写、运行、响应
最纯粹的形式上来看,自动化测试包含三个活动:编写测试,运行测试,响应测试失败。一个自动化的测试就是一小段代码,通常是一个函数或方法,调用到一个你要测试的更大系统一个独立的部分。测试代码配置好预期的环境,调用到系统,通常是已知的输入,并验证结果。有些测试非常小,只用一个简单的代码路径;其他的就会更大,可能包含整个系统,例如一个移动操作系统或网页浏览器。
例子11-1 展示了一个极其简单的Java测试,没有使用任何框架或者测试库。这不是你写整个测试集的方法,但是根本上来说自动化测试和这个简单的例子差不多。
在以前的流程里,专职的软件测试员盯着一个系统的新版本,测试每个可能的行为,和以前的QA流程不一样,现在构建系统的工程师在为自己的代码编写和运行自动化测试中发挥更为主动和整合的作用。即使是在QA部门很重要的公司里,开发自己写的测试也是司空见惯的。以目前系统开发的速度和规模来看,唯一能跟上的方法是在所有工程师成员中共享测试的开发工作。
当然,编写测试和编写好的测试是不一样的。培训数万名工程师写好的测试是非常困难的。在接下来的章节,我们会讨论我们关于编写好的测试学到的知识。
编写测试知识自动化测试流程的第一步。你写完测试之后,你需要运行。频繁地运行。自动化测试内部包含一遍一遍重复相同的动作,只有当有些测试失败之后才会需要人们的注意。我们会在第23章讨论持续集成(CI)和测试。通过把测试变成代码,而不是一系列手工的步骤,我们可以在每次代码变更的时候运行它们——每天可以轻松运行几千次。不像人类测试员,机器从来不会觉得疲倦或乏味。
用代码编写测试的另一个好处,是很容易为其在不同的环境执行模块化。测试Firefox中的Gmail行为,需要的工作并不比Chrome更多,前提是你有这两种系统的配置。日语界面或德语界面的测试可以用测试英语界面的代码完成。
处于活跃开发阶段的产品或服务不可避免地经历测试失败。衡量一个测试过程是否有效的方法就是看它怎么处理测试失败。允许失败的测试积累成堆,会让他们提供的价值黯然失色,所以无论如何也不能让这件事出现。在测试失败的数分钟内修复测试这件事,团队如果能够作为高优处理,就会保持高的信心,并且把失败快速隔离,因此能够产生更多的价值。
总之,一个健康的自动化测试文化鼓励每个人分享编写测试的工作。这样一种文化也确保测试能够经常运行。最后,并且也许是最重要的,快速修复失败的测试是非常重要的,只有这样才能在整个流程中保持较高的信心。
测试代码的好处
对于一些从没有足够测试文化组织出来的开发者,编写测试能够提升生产力和速率的想法可能正好相反。毕竟,编写测试的行为需要花费和实现一个需求的成本一样高(如果不是更高的话)。正相反,在谷歌,我们发现在软件测试上的投入对于开发者的生产力提供了几个关键好处:
更少的调试
和预期一样,测试过的代码提交的时候缺陷更少。关键的是,在整个生存周期之内,缺陷都更少;大部分缺陷都会在代码提交之前被捕获。谷歌的代码片段在其生命周期之内一般会被编辑几十次。代码会被其他团队或者自动化维护系统所改变。一次写好的测试在项目的整个生命周期会持续带来红利,并且防止高成本的缺陷和烦人的调试过程。项目的变化,或者项目的依赖,如果导致测试失败,会被测试基础设计检测到,并在问题发布到生产之前回滚。
变更中提升的自信
所有的软件都会变更。有完备测试的团队可以满怀信心地审查和接受项目的变更,因为项目所有重要的行为会被持续验证。这样的项目鼓励重构。保持现有行为重构代码的变化对于现有的测试应该(理论上)不需要改变。
文档质量提升
软件文档的不可靠性臭名昭著。从过期的需求到丢失的边界用例,文档常常和代码的关系非常脆弱。显然,一次针对一个行为的测试集可以看成可执行的文档。如果你想知道代码在某个特定用例下做什么,看一下这个用例的代码就好了。更好的情况是,当需求变化的时候,新的代码破坏一个存在的测试,我们就收到了一个清晰的信号:“文档”现在过时了。注意,只有测试在清晰和简洁地情况下才能作为文档存在。
更简单的审查
谷歌所有的代码在提交之前都会被至少一个其他的工程师审查(参见第9章)。如果代码审查中包含完整的测试能够说明代码的正确性、边界用例和错误条件,代码审查者就会花更少的时间验证代码是否符合预期。不用繁琐的大脑检查代码的每个用例,而是验证每个用例是否有一个通过的测试。
深入思考的设计
为新代码编写测试是一个测试代码本身API设计的实用的手段。如果新代码难以测试,常见的原因是被测代码有太多职责,或者难以管理的依赖。设计良好的代码应该是模块化的,避免紧耦合的且聚焦于特定职责的。更早修复设计问题常常意味着后期更少的返工。
更快、更高质量的发布
有一个健康的自动化测试集,团队就能有信心地发布新版本。谷歌的很多项目每天都发布一个新版本——甚至是有数百名工程师和每天数千次代码提交的大项目。没有自动化测试是不可能的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!