自动化功能测试的逻辑

持续交付涉及到软件开发从需求到上线、运维全生命周期的各个活动。其中很重要的一个活动就是测试。如果没有自动化测试,整个交付的节奏就会慢下来。接下来我们来聊一聊这背后的逻辑和如何才能把它做好。

软件开发中的自动化测试可以粗略的分为自动化单元测试和自动化功能测试。二者有很多的相似之处,但同样也有很多不同的关注点。本文主要关注的是 自动化功能测试

为什么要做自动化测试

如果你的软件是Web形态的,则用户可以通过浏览器来使用你的软件;如果你的软件是手机游戏,则用户需要有一台手机才能使用你的软件。而 功能测试 ,顾名思义,测的就是这些用户能够看到并使用的功能。

其实大多数软件开发流程都有功能测试这个环节。比如在传统的瀑布模式中,每次发布之前都需要进行一轮或者多轮的回归测试,以保证软件功能正确。在这种模式下,手动的功能测试 就是全部的测试活动。这些测试起到了防护网的作用,保证在发布前能够找到大部分的bug,并修复之。听起来不错,那为什么我们还是需要自动化的功能测试呢?

强哥的故事

让我们回到开发人员的视角来看看强哥的故事。强哥所在的团队使用迭代开发,每个功能开发完成之后,会让测试人员及时进行测试,每个月会进行一次发布。

作为一个有节操的开发人员,秉承为自己的代码负责的原则,强哥喜欢对自己所开发的功能进行细致的分析、开发和测试。这不,手上接了一个活,经过一番分析,梳理出了8个case:

花了一个小时完成了case 1,进行了一番完备的测试。又过了一个小时,case 2也完成了。强哥把case 1和case 2都再测试一遍。如此反复,等完成case 5的时候,自然需要从1到5都测试一遍。

这时候强哥心里有点犯嘀咕了:“case 1测了好几遍了,都的有点想吐了,而且case 5修改的代码其实跟case 1不是特别相关,要不先不测了,等到最后功能完成的时候在一起测试一遍吧”。于是在没有回归测试的前提下强哥完成了5、6、7、8这些case,而这时距离刚开始做这个功能已经过去两天了。

“终于做完了”,强哥长吁了一口气,“整体测一下吧”。不测不要紧,一测发现case 1、3,7都通不过了。

“还好又进行了测试”,强哥得意地想着,然后着手开始修复这些问题。经过一番艰苦卓绝的debug,终于定位了问题,并全部修复。这些工作又花掉了强哥3个小时。但强哥觉得这都是值得的,然后带着程序员的骄傲把功能交付给了测试人员。强哥考虑到的这些场景经过测试人员的测试,没有任何问题,一次通过。只是还有两个异常场景强哥没有考虑到,所以又回过头来修复了一下,这次倒是很快,10分钟就修好了。不过为了保险起见,强哥还是把所有的场景又回归了一遍。幸好,没什么问题,然后又提交给了测试人员。这次再没有什么问题了,通过!

就这样,强哥带着程序员的骄傲一次次高质量地完成了手里的工作。然后来到了迭代末尾,距离强哥完成上面那个功能已经过去三周了。要进行发布了,回归测试肯定还是要做的。第一轮结束后,发现了几个问题,其实一个竟然出现在强哥开发的功能中。“这个case我可是精心测过的,还测了好几遍,怎么挂了?”,强哥很不高兴。其他的开发同事跟强哥说:“别人做功能的时候,不可避免会影响已有的功能嘛,这都是正常现象,不然要回归测试干嘛呢?”

他说的好有道理,强哥一时竟无法反驳。在进行某一个小功能的开发时,可以在每次修改之后,都把这个功能所涵盖的case都回归一遍,但当这个功能提交给测试以后呢?别人在同一个代码库中做了某些修改,是不会对你的功能再进行一遍测试的。且不说别人都不知道你做了什么功能,就算知道,也不可能把所有的功能都再测试一遍呀,那要花多少时间呢?

修好这个问题之后,带着无奈和不甘心,强哥查了一下是哪次提交破坏了他的功能。让他惊讶的是,在他完成那个功能的两天后,也就是将近三周前,这个功能就坏掉了。强哥心里很不舒服。。。

自动化拯救世界

强哥所开发的软件是Web应用。后来强哥在一篇文章中知道了Selenium可以用来做浏览器的自动化测试。感觉豁然开朗:“如果我做的功能都能够用这个自动化测试来覆盖,那之前的那些问题大概都能解决了吧”。

  • 每次做完一个小功能时,补充一个自动化测试。后面每加一个功能只需要运行一遍这个不断增加的测试集即可。
  • 这些测试不光可以在我开发一个功能的时候帮我去运行这些重复性的验证,别人也可以在做完修改之后运行这些测试,以保证他没有破坏已有的功能。
  • 有了这些测试的保证,发布前的回归也会快的多。

为什么不做自动化测试?

听起来做自动化功能测试真的是一件很美好的事情,但实际上做的人并不多。那都是什么原因呢?

初始投入

做自动化测试的技术门槛不算高,但一开始还是需要一些投入。其中包括工具的学习和使用,基础框架的搭建、相关CI任务的搭建等。于是很多人都是这种状态:

自动化功能测试的逻辑

持续的投入

测试本身也是以代码的形式存在的,是代码就会腐化。于是编写测试也会变成一件成本越来越高的事情。另一方面,测试的有效性也很关键。比如如果一个用例声称会覆盖功能A,但是功能A出问题时,测试竟然没报错;或者当一个测试失败10次,其中有5次是因为测试环境出错、测试代码本身不稳定造成的,那估计团队也会越来越不想做写自动化测试了。

运行时间太久

功能测试一般都会模拟真正用户的行为,所以相对于单元测试来说,它们都很慢。如果对软件的所有行为编写功能测试,随着时间的推移,你会发现跑一边测试就需要一个小时,那还会有谁在每次提交之前运行一遍测试?

如何有效的做自动化测试

为了有效地进行自动化测试,你需要考虑很多。

选择正确的工具

要知道如何选择工具,首先要了解自动化测试的层次:

自动化功能测试的逻辑

测试框架本身非常关键,一个好的测试框架本身可以帮助你编写出可维护的测试用例,并提高编写效率。你可以使用junit、rspec之类的工具可以达到目的。但Cucumber和Robotframe等工具可以提供更多更好的功能,详情可以参看笔者的另一篇文章:Cucumber和Robotframework对比。如果你在进行基于Rack的应用程序的测试,那Capybara绝对是不二的选择。

一个好的测试驱动同样可以大幅提升用例开发的效率。在对网络通信的测试中,如果没有Scapy这个库的支持,恐怕编写用例的成本要增加一个数量级。在Web测试的上下文中,不光要考虑测试库是否能够满足你的功能性需求,还要考虑运行速度。基于真实浏览器的测试是很慢的,所以出现了PhontamJS这样的headless driver。

数据准备同样是编写测试中一个非常重要的步骤。你可以选择自己往数据库里面插数据,并进行清理,但如果有一个工具可以帮助你做类似的事情,岂不是很好?Ruby世界的FactoryGirl,和DatabaseCleaner可以在这方面提供很多帮助,节省你大量的时间。

能找到合适的工具是一件很幸运的事情。很多时候也可能找不到很好的工具。但这也没关系,就像编写产品代码一样,自己花时间写一个相应的库或者工具就好。

修复不稳定因素

环境不稳定、测试代码本身不稳定等因素都会导致大家越来越不想去运行测试。所以要在第一时间修复这些问题。举两个例子:

我工作过的一个软件系统会依赖与一个搜索引擎。由于搜索引擎是收费软件,为了节省成本,持续集成所使用的搜索引擎和手工测试使用的搜索引擎是同一个实例。所以有时候手工测试在其中注入数据时会导致自动化测试失败。后来又花钱买了一份liscense部署了一台新的实例,测试稳定性有了很大的提升。

如果发现了不稳定的测试,要第一时间修复。如果一时半会修复不了怎么办呢?我所在的团队曾经采用过的一种方法是打一个@quarantine的标签给这个测试,然后修改运行脚本,跳过所有打了这个标签的测试。然后再慢慢修复这个测试。这样就可以保证整个测试集是健康的,测试结果一定能够给你带来有效的信息。

方便测试的运行

如果我告诉你为了运行整个测试集,你需要手动重建数据库,然后feed一些测试数据到数据库中,然后启动应用程序A、B和C,然后再配置一系列的环境变量,然后再运行一串巨长的带着各种参数的命令,你是否还愿意跑这个测试?为何不把这些繁琐的工作写到一个脚本中,让别人简单地运行一个短短的命令就可以完成这个工作?这个脚本可以是一个shell脚本,但如果能够集成到项目所使用的构建工具中就更好了。在Ruby项目中可以集成到Rake中,在Node项目中可以集成到Gulp中,在Java项目中可以集成到Gradle中。

好了,现在你可以方便地运行所有的测试了。但是别忘了开发人员大部分的时间是在编写和调试单个用例,所以请保证你选用的测试框架或者IDE能够支持单个用例的运行和调试。

结合持续集成和PrePush

自动化功能测试的运行速度是比较慢的。即使你已经采用了类似PhantomJS这样的驱动,所有测试的整体运行时间还是不可避免的会很长。在持续集成服务器上可以采用并行的方式让多台机器来同时运行测试。但在本地如何并行运行呢?答案是挑选其中一部分我们觉得非常重要的测试来运行,而不是全部。具体的做法就是在我们认为很重要的用例上打上@prePush之类的标签,然后修改启动测试的脚本来只运行打了这些标签的测试。经验值是运行prePush的时间不要超过5分钟。

有了这个prePush脚本,就可以把它嵌在git push的hook中(假设你使用的是git),以保证每个人在提交代码的时候都能够运行一遍这些测试,从而确保自己的修改没有破坏别人的功能。

自动化测试不能解决什么问题

当然了,自动化测试也不是万能的。举个例子,强哥对自己能够想到的功能进行了完备的测试,但是有些异常场景如果没有考虑到,自然也就没法编写相应的测试。所以需求分析和用例设计其实是一个单独的、但也同等重要的课题。

上文提到了很多次功能测试运行时间太久的问题,这个问题除了使用prePush和并行运行之外还有什么办法可以解决吗?答案就是自动化单元测试和测试金字塔!当然本文就不对这个话题展开讨论。

不忘初心

自动化功能测试本身并不是目的,并不是你跟别人说:“看,我的测试覆盖率超过90%”,你的项目就成功了。代码没bug、开发可以快速交付、市场认可才是最终目的,而自动化功能测试只是实现这些的目标的一个手段而已。即不是唯一的手段,也不是必不可少的手段。

上一篇:发布与分支


下一篇:《Android框架揭秘》——2.5节应用程序Framework源码级别调试