简介
有很多文章探讨过软件开发过程中的测试误区。但大多着眼于底层编码细节,而且总是着眼特定的技术或编码语言。
本文我会后退一步,从更高层次上总结出一些语言无关的测试误区。希望无论你偏好什么语言,都能从本文学习到有用的东西。
术语
不幸地是,测试术语并未达成共识。
关于集成测试、模块测试和端到端测试的区别,100个测试人员可能会有100种不同的答案。
为了更好地说明测试误区,我会重点关注如下所示的测试金字塔的定义。
如果你没听说过测试金字塔,最好在阅读本文之前了解一下。推荐文章:
>>The forgotten layer of the test automation pyramid (Mike Cohn 2009)
>>The Test Pyramid (Martin Fowler 2012)
>>Google Testing blog (Google 2015)
>>The Practical Test Pyramid (Ham Vocke 2018)
测试金字塔本身就值得讨论,特别是关于每一层所需的测试量。
本文引用金字塔只是为了定义底层的两种测试类型。
注意,本文中并未提及UI测试(位于顶层)
(为了简化起见,而且UI测试的误区会单独说明)。
上面提到的两种主要测试类型,单元测试和集成测试,定义如下:
单元测试
顾名思义,是一种广泛应用的测试类型。
特点:
1.其与源码放在一起并可直接访问源码。通常由xUnit框架或其他类似的库执行。
2.测试直接运行在源码上,能够有一个全局的视角。
3.测试一个单独的类/方法/函数(或某业务功能的最小工作单元),其他的都用stub/mock。
集成测试
也叫服务测试,或者模块测试
关于整个模块。
特点:
1.一个模块可以是一个包含若干类/方法/函数的集合、模块、子系统或甚至应用本身。
2.通过传递输入数据并检查输出数据来校验模块。
通常需要先进行某些部署/启动/安装。
3.外部系统可以完全模拟、替换(比如使用内存数据库替换实际数据库)、或者根据业务状况决定是否使用真实的外部依赖。
4.相比于单元测试,集成测试需要用更专业的工具来准备测试环境或者交互/验证系统。
第二种测试类型定义五花八门,关于测试命名的争议也是繁多。而且集成测试的“范围”也是颇具争议,尤其涉及到其访问应用的本质(是黑盒还是白盒测试以及是否允许mock)
有一些经验法则可以帮助判断测试类型。
如果:
测试使用了数据库
测试通过网络调用了其他模块/应用
测试使用了外部系统(比如队列,或者邮件服务器)
测试进行了文件读写或者其他IO操作
测试并不依赖源码而是依赖部署好的app的二进制文件
那么该测试就是集成测试而不是单元测试。
说完命名的问题,我们可以深入测试误区列表了。
测试误区的顺序大概按照其出现的频率排序。经常出现的会排在前面。
软件测试误区列表
1.只有单元测试,没有集成测试
2.只有集成测试,没有单元测试
3.测试种类错误
4.测试错误的功能
5.测试内部实现
6.过于关注测试覆盖率
7.存在不稳定或者耗时的测试
8.手动运行测试用例
9.把测试代码当做二等公民
10.没有把产品bug转化测试用例
11.拿TDD当做信仰
12.不看说明书直接写测试用例
13.因为无知而认为测试不好
误区1: 只有单元测试,没有集成测试
这是中小型企业中经常存在的问题。公司开发的应用除了单元测试(测试金字塔最底层)外啥也没有。
通常缺少集成测试的原因是下面几种:
1.公司没有高级开发人员。团队中只有刚毕业的初级开发人员,基本只见过单元测试。
2.曾经采用过集成测试,但是又废弃了,因为带来的麻烦比收益要多。单元测试好维护,所以占了上风。
3.设置应用的运行环境非常具有“挑战性”。在产品中“测试”功能。
对于第1条真的没啥好说的。每一个高效的团队都至少有一些能够向其他人传授良好实践的导师。第2条会在误区5、7、8中详述。
然后看下最后一个问题:难以设置测试环境。
不要误会,有的应用确实难以测试。
我曾经遇到过一组REST应用,所在主机实际上依赖一种特殊的硬件。而该硬件只存在于生产环境,所以进行集成测试非常有挑战性。但这只是一个极端情况。
对于公司编写的一般web或后端应用来说,测试环境设置应该是没有问题的。
随着虚拟机和最近容器技术的兴起,这一点更加毋庸置疑。
如果准备测试的应用很难设置测试环境,那你首先要做的是修正设置过程而不是开始测试。
但是为什么集成测试是必须的呢?
原因是有一些问题只能由集成测试检测出来。
典型的例子是所有同数据库操作相关的东西。
比如数据库事务,数据库触发器和其他只有集成测试才会访问到的存储过程。无论是你还是其他团队开发的模块间的连接需要集成测试(也叫契约测试)。
任何需要验证性能的测试,从定义上来说都是集成测试。
下面是需要集成测试的理由:
基本上,应用中任何存在横切关注点的地方都需要集成测试。
随着近期微服务架构的火爆,集成测试变得更加重要,因为即使你自己的服务之间也需要按照契约进行交互。如果这些服务由其他团队开发,你需要自动化的方式来校验接口契约是否有效。而这些只能由集成测试完成覆盖。
总结,除非你开发的模块极度独立(比如linux命令行工具),否则你真的需要集成测试来找出单元测试无法发现的bug。
误区2: 只有集成测试,没有单元测试
这与上一个误区相反。这个误区在大公司和大项目中更常见。
几乎所有情况背后都与开发人员认为单元测试没有价值,只有集成测试才能发现问题的看法有关。
相当多经验丰富的开发人员认为单元测试是在浪费时间。
如果和他们深入交流之后,通常你会发现在过去,高层曾强制他们通过编写琐碎的单元测试来提高测试覆盖率。
理论上你可以在软件项目中仅采用集成测试。但实际上这会使测试代价变高(开发时间和构建时间)。
在上一节的表中,我们知道集成测试同样可以用于发现逻辑错误,所以集成测试就可以用该方式"取代"单元测试。
但是这种方式是长期可行的吗?
集成测试的复杂性
举个例子。假如你有一个服务,包含4个方法/类/函数。
模块上数字表示圈复杂度,换句话说为其独立代码路径条数。
>>Mary,一个"理论派"研发,想要为服务编写单元测试(因为她认为单元测试确实有价值)。
那么为了覆盖所有可能的场景她需要写多少用例呢?
显然写2+5+3+2=12个独立的单元测试就可以完全覆盖这些模块的业务逻辑。
记住这只是单独一个服务的数字,而Mary负责的应用包含很多服务。
>>Joe, 一个"暴躁派"研发,相反地,他从不认可单元测试的价值。他认为单元测试是在浪费时间,并且只在该模块中写集成测试。
那么他需要写多少集成测试用例呢?
他开始计算一个请求在该服务中所有可能的路径。
很明显,代码路径所有可能场景是2*5*3*2=60。
那么这就意味着Joe真的会写60个集成测试用例吗?
当然不是!
他会偷懒。他会选出集成测试中一个感觉"有代表性"的子集。
这个"有代表性"的子集可以让他通过最小的工作来达到足够的覆盖率。
理论上听起来很简单,但是很可能变得问题多多。
实际情况是,这60个用例中的代码路径并不是平等的。
其中有极端情况。
比如,如果模块C中有3种不同的代码路径。其中一个是非常特殊的用例,只有C从B中得到一个特殊的输入时才会复现,而B只有从A中得到一个特殊的输入之后才会产生这种特殊输出。
这意味着该特殊场景可能需要一个非常复杂的设置才能获取到可能触发这种极端情况的输入,以触发模块C的特殊条件。
>>而Mary,只需要用一个简单的单元测试用例即可复现该极端用例,不需要增加任何复杂度。
这意味着Mary可以只给服务写单元测试吗?
这样就变成了误区1。
为了避免这种情况,她两种都需要写。
她应该完成所有的单元测试用例以覆盖业务逻辑,然后写1到2个集成测试用例用于保证系统的其他部分按照预期运行(也就是保证这些模块正常运行的东西)。
系统需要的集成测试应该关注其他模块。
而内部业务逻辑应该交给单元测试。
Mary的集成测试应该关注序列化/反序列化,队列通信和系统数据库交互的测试。
最后,集成测试的数量应该比单元测试的数量要少的多(符合测试金字塔的形状)
集成测试慢
抛开复杂性,集成测试的第二个大问题是速度。
通常集成测试要比单元测试慢一个数量级。
单元测试只需要应用源码。基本上是CPU密集型。
集成测试能够同外部系统进行IO操作,使得其很难高效运行。
让我们假设一组数据来获取一个运行时间差异的基本认知。
·每个单元测试耗时60ms(平均)
·每个集成测试耗时800ms(平均)
·应用有40个像上一节中那样的服务
·Mary为每个服务写了10个单元测试用例和2个集成测试用例
·Joe为每个服务写了12个集成测试用例
现在我们来算一下。
注意我这里假设了Joe已经找到了集成测试用例的最佳子集,让他能够得到和Mary一样的代码覆盖率(实际应用中是不可能的)
总的运行时间相差巨大。每次代码变更后运行1分钟和6分钟的差异相当大。集成测试800ms的运行时间已经是相当保守了。我见过集成测试用例集中单个用例耗时达数分钟之久的。
总的来说,尝试只用集成测试来覆盖业务逻辑是非常耗时的。即使你用CI来自动化运行测试用例,反馈循环(从提交到获取测试结果的时间)也会非常长。
集成测试更难debug
最后一个关于为什么只用集成测试(没有任何单元测试)的原因是调试失败用例的会更耗时。
因为集成测试是测试多个软件模块(根据定义), 当用例失败时,失败可能来自于任一个被测模块。定位问题的难度会随着模块数量的增加而增加。
当一组集成测试用例失败时,你需要能够知道失败原因并修复。集成测试的复杂度和广度使得其极难debug。我们再一次假设,你的应用只有集成测试。而你正在开发的应用是一个典型的电商网站。
团队中的研发人员(或者你)提交一次新的代码,触发集成测试,结果如下所示:
作为一个研发人员,你看着测试结果,发现名为"顾客买一个货物"的用例失败了。在电商应用这个上下文里,这句话并没有什么卵用。
有很多种情况能导致这个用例失败。
除了深入日志和测试环境之外没有其他办法得知为什么用例失败原因。有些情况下(和更复杂的应用)唯一有效的debug方法就是检查代码,本地复现测试环境,然后运行集成测试用例,看看它在本地开发环境中如何失败。
现在想象你和Mary一起开发这个应用,所以你们有了单元测试和集成测试。你的团队提交了代码,运行所有测试之后得到如下结果:
现在两套测试用例都失败了:
··"顾客买一个货物"像之前那样失败了(集成测试)
··"特殊折扣测试"同样失败了(单元测试)
现在就很容易知道应该从哪儿开始排查问题了。
你可以直接查看 Discount 功能的源码,定位bug并修复,99%的情况下集成测试中的bug也会随之修复。
在集成测试同时或之前出现单元测试失败,会让你的问题排查过程轻松许多。
快速总结你为什么需要单元测试
这是本文最长的一个章节,但是我认为很有必要。总的来说,理论上你可以只用集成测试,但实际上:
1.单元测试更容易维护
2.单元测试更容易复现极端用例和低频场景
3.单元测试运行速度更快
4.单元测试的失败比集成测试更容易修复
如果你只有集成测试,那么你是在浪费开发时间和公司成本。你需要同时准备单元和集成测试。他们并不互斥。
网络很多文章宣扬仅采用一种测试类型。这些文章都被误导了。很遗憾但是真的。
误区3: 测试种类错误
既然我们知道了为什么同时需要多种测试类型(单元和集成),现在我们需要决定每类中需要多少测试用例。
没有快速可用的法则,这取决于你的应用。
重要的是你需要花时间去理解哪种类型的测试对你的应用最有意义。
测试金字塔对你测试用例数量仅仅是一个参考。
它假设你写的是商业网站应用,但现实并非总是如此。
Linux命令行工具
假设应用是命令行工具。
其读取一个特殊格式的文件(比如CSV),一顿转换之后导出另一个格式(比如JSON)。
应用是独立的,不与其他系统或者网络交互。
转换是一个复杂的数学过程, 对应用的结果正确性要求严格(可以慢但是必须对)
在这个粗糙的例子中,你需要:
1.大量用于数学方程的单元测试
2.一些用于CSV读取和JSON写入的集成测试
3.没有GUI测试,因为没有GUI
那么这个项目的构成看起来是这样:
单元测试主宰了这个例子,而且形状完全就不是金字塔。
支付管理系统
你正在开发一个新的项目,会被加入到现有的一个大型企业系统集合中去。
该应用是一个支付网关,用于处理外部系统的支付信息。
该系统需要将所有交易的信息记录至一个外部数据库,需要同外部支付供应商通信(比如 Paypal, Stripe, WorldPay),需要也将交易细节发送给另一个发票系统。
在这个粗糙的例子中,你需要:
1.几乎不写单元测试,因为没有业务逻辑
2.大量的集成测试用于外部通信、数据存储和发票系统
3.没有UI测试,因为没有UI
那么这个项目的构成看起来是这样:
集成测试主宰了这个例子,而且形状完全就不是金字塔。
网页生成器
你正在开发一个全新的项目,其会重新定义人们开发网站的方式,通过提供独一无二的方式在浏览器中完成创建网页应用。
应用是一个图形化设计页面,其中一个工具箱包含所有可能出现的HTML元素,并可随其它预定义模块一起添加至一个web页面中去。
应用可以通过应用市场获取新的模板。网页生成器交互非常友好,允许你通过拖拽/放置元素,改变大小,编辑属性,变换颜色和外观。
在这个粗糙的例子中,你需要:
1.几乎没有单元测试因为没有业务逻辑
2.对接应用市场的集成测试
3.大量的UI测试用于保证用户体验就像广告中的一样
那么这个项目的构成看起来是这样:
UI测试主宰了这个例子,而且形状完全就不是金字塔。
我用了一些极端的例子说明为什么你需要理解你的应用并仅关注那些能够给你带来价值的测试类型。我曾经见过没有集成测试的"支付管理系统"和没有UI测试的"网页生成器"。
网上有些文章(我没打算po出链接)谈论一个你需要或不需要的集成/单元/UI测试的具体数字。
所有的这些文章都建立在你的例子中不一定成立的假设之上。
由于篇幅原因,软件测试的误区先介绍到这里。
下周同一时间,Qtest之道公众号,我们继续带给大家软件测试的误区讲解~
小编
Qtest是360旗下的专业测试团队!
是WEB平台部测试技术平台化、效率化的先锋力量!
领取专属 10元无门槛券
私享最新 技术干货