29自动化测试:如何把Bug杀死在摇篮里
前不久我所在项目组完成了一个大项目,把一个网站前端的jQuery代码全部换成React代码,涉及改动的项目源代码文件有一百多个,变动的代码有几千行,最终上线后出乎意料的稳定,只有几个不算太严重的Bug。
能做到重构还这么稳定,是因为我们技术水平特别高吗?当然不是。还是同样一组人,一年前做一个比这还简单的项目,上线后却跟噩梦一样,频繁出各种问题,导致上线后不停打补丁,一段时间才逐步稳定下来。
这其中的差别,只是因为在那次失败的上线后,我们总结经验,逐步增加了自动化测试代码的覆盖率。等我们再做大的重构时,这些自动化测试代码就能帮助我们发现很多问题。
当我们确保每一个以前写好的测试用例能正常通过后,就相当于把Bug杀死在摇篮里,再配合少量的人工手动测试,就可以保证上线后的系统是稳定的。
其实对于自动化测试,我们专栏已经多次提及,它是敏捷开发能快速迭代很重要的质量保障,是持续交付的基础前提。
所以今天我将带你一起了解什么是自动化测试,以及如何在项目中应用自动化测试。
为什么自动化测试能保障质量?
自动化测试并不难理解,你可以想想人是怎么做测试的:首先根据需求写成测试用例,设计好输入值和期望的输出,然后按照测试用例一个个操作,输入一些内容,做一些操作,观察是不是和期望的结果一致,一致就通过,不一致就不通过。
自动化测试,就是把这些操作,用程序脚本来完成的,本质上还是要输入和操作,要检查输出是不是和期望值一致。只要能按照测试用例操作和检查,其实是人来做还是程序来做,结果都是一样的。
不过,自动化测试有一个手工测试没有的优势,那就是可以直接绕过界面,对程序内部的类、函数进行直接测试,如果有一定量的自动化测试代码覆盖,相对来说软件质量是更有保障的。
而且,一旦实现了自动化,每测试一次的成本其实大幅降低了的,几百个测试用例可能几分钟就跑完了。尤其是每次修改完代码,合并到主干之前,把这几百个测试用例跑一遍,可以有效地预防“修复一个Bug而产生新Bug”的情况发生。
但现阶段,自动化测试还是不能完全代替手工测试的,有些测试,自动化测试成本比手工测试成本要高,比如说测试界面布局、颜色等,还是需要一定量的手工测试配合。
有哪些类型的自动化测试?
现在说到自动化测试,已经有很多的概念,除了大家熟悉的单元测试,还有像集成测试、UI测试、端到端测试、契约测试、组件测试等。而很多时候同一个名字还有不同的解读,很容易混淆。
在对自动化测试类型的定义方面,Google的分类方法我觉得比较科学:根据数据做出决策,而不仅仅是依靠直觉或无法衡量和评估的内容。Google将自动化测试分成了三大类:小型测试、中型测试和大型测试。
假设我们有一个网站,是基于三层架构(如下图所示),业务逻辑层的类叫UserService类,数据访问层的类叫UserDA,我们将以用户注册的功能来说明几种测试的区别 。
小型测试
小型测试是为了验证一个代码单元的功能,例如针对一个函数或者一个类的测试。我们平时说的单元测试就是一个典型的小型测试。
比如说UserService这个类,有一个注册用户的函数,现在要对它写一个单元测试代码,那么看起来就像下面这样:
通过这样的测试代码,就可以清楚的知道UserService类的create这个函数是不是能正常工作。
小型测试的运行,不需要依赖外部。如果有外部服务(比如文件操作、网络服务、数据库等),必须使用一个模拟的外部服务。比如上面例子中我们就使用了FakeUserDA这个模拟的数据库访问类,实际上它不会访问真实的数据库。这样可以保证小型测试在很短时间内就可以完成。
(小型测试,图片来源:《Google软件测试之道》)
中型测试
中型测试是验证两个或多个模块应用之间的交互,通常也叫集成测试。
如果说要对用户注册的功能写集成测试,那么就会同时测试业务逻辑层的UserService类和数据访问层的UserDA类。如下所示:
对于中型测试,可以使用外部服务(比如文件操作、网络服务、数据库等),可以模拟也可以使用真实的服务。比如上面这个例子,就是真实的数据库访问类,但是用的内存数据库,这样可以提高性能,也可以减少依赖。
至于中型测试要不要使用模拟的服务,有个简单的标准,就是看能不能在单机情况下完成集成测试,如果可以就不需要模拟,如果不能,则要模拟避免外部依赖。
(中型测试,图片来源:《Google软件测试之道》)
大型测试
大型测试则是从较高的层次运行,把系统作为一个整体验证。会验证系统的一个或者所有子系统,从前端一直到后端数据存储。大型测试也叫系统测试或者端对端测试。
如果说要对用户注册写一个端对端测试的例子,那么看起来会像这样:
对于大型测试,通常会直接使用外部服务(比如文件操作、网络服务、数据库等),而不会去模拟。比如上面这个例子,就是直接访问测试环境的地址,通过测试库提供的API操作浏览器界面,输入测试的用户名密码,点击注册按钮,最后检查输出的结果是不是符合预期。
(大型测试,图片来源:《Google软件测试之道》)
区分测试类型的依据是什么?
以上就是主要的自动测试类型了。捎带着补充一个测试类型,那就是契约测试,这个测试最近出现的频度比较高,主要是针对微服务的。其实就是让微服务在测试时,不需要依赖于引用的外部的微服务,在本地就可以模拟运行,同时又可以保证外部微服务的接口更新时,本地模拟的接口(契约)也能同步更新。对契约服务更多的说明可以参考这篇文章:《 聊一聊契约测试 》
那么契约测试,属于大型测试还是中型测试呢?
Google针对这几种测试类型列了一张表,根据数据给出了明确区分:
(图片来源:Google Testing Blog)
结合上面的表格其实就很好区分了:
- 小型测试,没有外部服务的依赖,都是要模拟的;
- 中型测试,所有的测试几乎都不需要依赖其他服务器的资源,如果有涉及其他机器的服务,则本地模拟,这样本机就可以完成测试;
- 大型测试,几乎不模拟,直接访问相关的外部服务。
所以现在你应该就知道契约测试,也是中型测试的一种了,因为它不需要依赖外部服务,本机就可以完成测试。
为什么中型测试这么看重能单机运行呢?因为这样才方便在持续集成上跑中型测试,不用担心外部服务不稳定而导致测试失败的问题。
上面的表中还反应出一个事实:越是小型测试,执行速度越快,越是大型测试,执行速度越慢。 通常一个项目的小型测试,不超过一分钟就能全部跑完,一个中型测试,包括一些环境准备的时间,可能要几分钟甚至更久,而大型测试就更久了。
另外越是大型测试,写起来的成本也相应的会更高,所以一般项目中,小型测试最多,中型测试次之,大型测试最少。 就像下面这张金字塔图一样。所以我们也常用测试金字塔来区分不同类型的测试粒度。
(测试金字塔,图片来源: TestPyramid )
如果你对测试类型很感兴趣,可以参考《测试金字塔实战》这篇文章作为补充。
怎么写好自动化测试代码?
很多人认为写自动化测试很复杂,其实测试代码其实写起来不难,包含四部分内容即可,也就是:准备、执行、断言和清理,我再把第一段代码示例贴一下:
第一步就是准备,例如创建实例,创建模拟对象;第二步就是执行要测试的方法,传入要测试的参数;第三步断言就是检查结果对不对,如果不对测试会失败;第四步还要对数据进行清理,这样不影响下一次测试。
上面还有几个测试代码示例,都是这样的四部分内容。
这是针对写一个自动化测试的代码结构。对于同一个功能,通常需要写几个自动化测试才完整。
一个完整的自动化测试要包括三个部分的测试:
- 验证功能是不是正确: 例如说输入正确的用户名和密码,要能正常注册账号;
- 覆盖边界条件: 比如说如果用户名或密码为空,应该不允许注册成功;
- 异常和错误处理: 比如说使用一个已经用过的用户名,应该提示用户名被使用。
所以你看,写一个测试代码并没有你想的那么复杂,那还有什么理由不去写测试呢?
如何为你的项目实施自动化测试?
现在你了解了有哪些类型的测试,如何写自动化测试代码,也许迫不及待想在项目中实施自动化测试。
选择好自动化测试框架
要写好自动化测试代码,首先要找对自动测试化框架。不同的语言,不同的平台,测试的框架都不一样。好在现在搜索引擎很方便,根据“你的语言+自动测试框架”的关键字,就能找到很多的结果。这里我也帮你找了一些,供参考。
- Web前端
Jest: Facebook的前端测试框架;
Mocha:历史悠久的一个JS测试框架;
Nighwatch: 一个API很简单,但是功能很强大,可以直接操作浏览器的自动测试框架。
- iOS开发
可以参考这篇文章《iOS自动化测试框架对比》。
- 安卓开发
可以参考这篇文章《Android 谈谈自动化测试》。
在持续集成环境上跑你的自动化测试
选好自动化测试框架后,你的自动化测试代码,其中的小型测试和中型测试,最好要能在持续集成环境上运行起来。
让自动化测试在持续集成上运行非常重要,只有这样才能最大化地发挥自动化测试的作用。
因为持续集成,会强制测试通过才能合并代码,在合并代码之前就能知道测试是不是都通过了,可以帮助程序员获得最直观的反馈,知道哪里可能存在问题,这样才能真正做到防患于未然,把Bug杀死在摇篮里。
下图描述的就是自动测试配合持续集成的一个标准流程:
- 在提交代码前,先本地跑一遍单元测试,这个过程很快的,失败了需要继续修改;
- 单元测试成功后就可以提交到源代码管理中心,提交后持续集成服务会自动运行完整的自动化测试,不仅包括小型测试,还有中型测试;
- 通过所有的测试后,就可以合并到主分支,如果失败,需要本地修改后再次提交,直到通过所有的测试为止。
(图片来源: Microservice Testing: Unit Tests )
新项目和老项目的不同策略
如果是新项目,那么可以在一开始就保持一定的自动化测试代码的覆盖率,你甚至还可以试试测试驱动(TDD)的开发模式,也就是先写测试代码,再写实现代码,保证测试通过,最后对代码进行重构。
(图片来源:郑晔 《10x程序员工作法》专栏)
如果是老项目,短期内要让自动化测试代码有覆盖是有难度的,可以先把主要的功能场景的中型测试写起来,这样可以保证这些主要功能不会轻易出问题。
后面在维护的过程中:
- 增加新功能的时候,同步对新功能增加自动化测试代码;
- 修复Bug的时候,针对Bug补写自动化测试代码。
这样一点一点,把自动化测试代码覆盖率加上去。
如果时间紧任务重,来不及写自动化测试怎么办?
确实遇到时间紧的情况,我建议你要优先保证中型测试代码的覆盖,因为这样至少可以保证主要的用户使用场景是正常的。然后把来不及完成的部分,创建一个Ticket,放到任务跟踪系统里面,后面补上。
总结
今天我带你一起学习了关于自动化测试有关的知识。自动化测试,分为三类:
- 小型测试,主要针对函数或者类进行验证,不调用外部服务,执行速度快;
- 中型测试,主要验证两个或多个模块应用之间的交互,可能会调用外部服务,尽可能让所有测试能在本机即可完成,执行速度比较快;
- 大型测试,对服务整体进行验证,执行速度慢。
写好单元测试代码,基本结构就是:准备、执行、断言和清理;基本原则就是:
- 要验证正确性;
- 覆盖边界条件;
- 验证是否有异常和错误的处理。
自动化测试,一定要配合好持续集成,才能最大化发挥其效用。
对于自动化测试的实施,开头是最难的,因为需要花时间选择自动化测试框架,需要针对自动化测试框架搭建环境,甚至要去搭持续集成环境。但搭建持续集成和搭建自动化测试环境,并且保证持续更新维护自动测试代码,这个技术投资,一定是你在项目中最有价值的投资之一。
搭建持续集成环境和集成自动化测试框架的事情,要作为一个正式的项目任务去做,当作一个很重要的任务去推进。
课后思考
你所在项目中,自动化测试代码覆盖如何?保持高覆盖率的主要阻力或者障碍是什么?打算怎么改善项目中自动化测试代码的覆盖?欢迎在留言区与我分享讨论。
感谢阅读,如果你觉得这篇文章对你有一些启发,也欢迎把它分享给你的朋友。
本文收藏来自互联网,用于学习研究,著作权归原作者所有,如有侵权请联系删除
markdown @tsingchan
部分引用格式为收藏注解,比如本句就是注解,非作者原文。