1.2 软件自动化测试工具的挑战
上一节我们对软件测试各个阶段的自动化需求进行了分析,这一节我们讨论一下软件自动化测试工具所面临的一些挑战。
1.2.1 测试用例的复用能力
所谓测试用例的复用能力是指,测试用例可以复用在同类产品的不同型号,甚至不同产品、相同功能的测试上,并且能够根据不同的被测对象,决定测试点的通过条件,在回归测试阶段,这一点显得尤为重要。
通常根据软件设计模式分层设计的思想,测试用例作为最上层的业务层面,其很多的操作行为应该定义得足够抽象,通过中间层来解除测试用例和实际被测对象之间的耦合关系。这一方法非常常见,比如操作系统,其驱动程序就是一个中间层,解耦了用户对硬件的操作,又比如嵌入式系统中的HAL层,也就是硬件抽象层。这样的设计,可以将底层具体的操作封装起来,不暴露给上层用户,上层用户调用的函数或方法就是一个很抽象的操作。如果有更多类型的硬件添加进来,或者硬件功能发生了更改,只需要更新相应的驱动程序或HAL层的代码,用户完全感知不到底层的改动。
因此,在测试用例中,测试的具体逻辑操作是一个抽象的操作。比如登录网站和输入用户名、密码,这些操作并不会定义登录网站的URL,以及用户名输入控件的XPATH。至于访问什么URL,控件的XPATH是什么,则交由底层去封装,如图1-1所示。
图1-1 测试用例与被测试对象解耦
这样,在底层操作中,我们可以根据不同的URL来决定用户名和密码框的XPATH。
这看上去似乎是测试用例设计者需要解决的问题,其实在测试工具的层面就需要支持这样的一些特性。比如,当测试用例需要请求测试资源的时候,能够根据不同的资源类型返回合适的请求内容。
1.2.2 测试用例的扩展能力
测试用例的扩展能力是基于复用能力的,在测试用例无法被复用的情况下,测试用例是否有能力经过一些简单的修改,达到复用的目的呢?
比如,一个软件的配置原本采用Telnet方式,但是后来为了安全性,全部改成了SSH方式。假设我们原本开发测试用例的时候,让用户在测试用例中直接调用Telnet相关的对象,那么这种结果就是灾难性的,因为如果要将测试用例修改成支持SSH连接的模式,就需要对所有的测试用例进行修改。如果想让测试用例既支持Telnet又支持SSH,那么这种修改就会让人更为恼火。如果你不关心配置模式是Telnet还是SSH,那么测试工具就应该把具体的Telnet封装起来,提供给开发者调用,如图1-2所示。
图1-2 命令行的抽象
命令行工具的封装部分,在第10章有详细的设计和实现的案例。
1.2.3 测试工具的扩展能力
随着产品项目的发展,我们可能会逐步地引入更多的测试设备和测试工具,并且我们希望能够将这些设备和工具集成到自动化测试中。新的设备或工具可能是用于替换旧型号的设备或工具,也可能是为了支持新的测试方法。
在交换路由测试领域,早期是思博伦公司的Smartbits和IXIA公司的IxExplorer工具应用比较广泛,所以一开始测试用例是基于这两种设备开发的。随着仪表厂商的产品的更新换代,Smartbits和IxExpolorer被TestCenter和IxNetwork甚至IxLoad代替了,老的产品逐渐结束生命周期。作为企业来说,也会逐渐更新设备,需要对原本的自动化测试工具进行扩展,让它支持新的测试仪表。在增加了新的测试仪表支持之后,原先开发的测试用例可以在新的测试仪表上运行。如果一家企业除了做交换路由,还开始涉及WIFI的产品,那么就需要购买WIFI产品的测试设备,比如IxVeriwave,于是为了开发自动化测试用例,就需要让自动化测试工具增加对IxVeriwave的支持。
这些例子所描述的情况可能对于很多团队来说并不迫切,比如要购买新的仪表,大多数企业还是会做计划、做预算,然后购买,这样会有足够的时间去做自动化测试上的重构。下面我们再来考虑一些更普遍的情况,比如在一套系统的测试中,网络系统方面加入了HTTP代理服务器的支持,所以作为测试工具,就需要在测试资源中增加代理服务器的描述,并且测试用例能够对其进行一定的配置操作。后来又增加了SOCK4或SOCK5的代理的支持,并且支持认证功能,我们又需要对新的代理服务器进行扩展和支持。
在大多数情况下,这些扩展的用例体现在以下几个方面:
(1)对新设备或工具的配置过程的封装,我们不可能让测试用例的开发者直接调用设备的接口,所以一般来说,我们都会将设备或工具提供的接口封装成符合我们自己规范的接口或者模块。
(2)能够快速添加支持。比如一个测试工具在构建测试拓扑配置的时候,在添加了新的工具或设备后,能够让这些配置快速支持新设备的描述信息,让测试用例能够调用到。
(3)在添加了相同功能的设备后,对于相同的逻辑操作的测试用例,甚至不需要修改测试用例,比如上面举的交换路由测试的例子。
1.2.4 灵活的测试调度能力
自动化测试用例开发完成以后,最终的目的是执行。而不同测试阶段的执行要求也不尽相同,甚至在同一个阶段内,不同工程师都会有不同的执行需求。
最基本的需求就是,能够将自己想要执行的测试用例放在一个测试列表中,并且对其进行执行,这应该是目前几乎所有的自动化测试的执行模式。当然这种模式会有一定的问题——针对不同的测试目的,可能会建立很多不同的测试用例列表。比如,某个团队针对可用性测试有一个测试用例列表,包含了仅仅10个测试用例,针对回归测试有1000个测试用例,由于数量太多,高优先级的测试用例中有一个测试用例列表可能包含了400个测试用例,低优先级的测试用例中有一个测试用例列表包含了另外600个测试用例,针对一些压力测试,需要把一个测试用例执行很多遍,此时就需要把测试用例在这个列表中复制很多次,类似这种要求还有很多。
所以自动化工具在提供测试用例列表功能的同时,需要提供灵活的调度和控制,以便少量的测试用例列表就能满足不同执行者的需求。
比如,我们可以规定,某次执行只执行测试列表中高优先级的测试用例,或者只执行某种类别的测试用例,指定测试某个测试用例的执行次数,甚至可以提供一定的参数配置传递到测试用例本身。比如对于每日回归测试,测试用例中的某个测试点挑选3个随机值,对于周末回归测试,同样的测试点挑选10个随机值。
这种灵活配置和调度能够让执行者更少地维护测试用例列表,通过一些参数配置来达到不同的用例执行。
1.2.5 测试结果和报告
自动化的测试结果和报告包含以下内容:
(1)单个测试用例的输出。
(2)测试执行的结果。
(3)测试报告。
1.2.5.1 单个测试用例输出
单个自动化测试用例的测试结果的展示,直接关系到结果分析的效率。测试执行人员往往希望,在拿到一个新的测试用例并执行后,能够根据输出的结果快速地判断测试用例执行了哪些操作,结果如何。如果失败,原因在哪里,同时需要失败步骤的错误信息及日志信息。
通常,单个测试用例的结果输出有主动输出和被动输出。主动输出指的是,测试用例的开发者尝试告诉执行者,这个步骤的目的、期望结果、实际结果等,而被动输出指的是,从被测系统、测试工具或测试工具本身产生的结果和日志信息。
很多团队使用日志模式的输出记录,因为很多语言工具提供了相应的日志工具,这会让结果输出变得比较简单,比如Python的logging包和Java的log4j等。在这种输出模式中,虽然对日志进行了等级划分,但是总体来说,所有的信息是平面的,所有的信息都会交织在一起。这很容易理解,比如一个测试RESTful API调用的测试用例,假设这个API的URL是https://<test_ip>/v1/information,测试用例的步骤如下:
第一步,调用API,方法是GET。
第二步,检查返回值是否是200。
第三步,检查返回信息是否是期望的返回信息。
如果API调用的库本身会输出日志,那么这个日志会是这样的:
如果这样的内容过多,测试工程师进行结果分析的时候就会很麻烦,因为并不是所有的被动日志都是有用的信息。而用户往往希望看到和上述列表相同模式的输出,并暂时隐藏不必要的技术信息,这样结果就会一目了然。有时候用户可能只对失败的步骤产生兴趣,只需要失败时候的具体信息和日志。
因此,对于单个测试用例的输出,需要打破平面的输出模式,利用前端工具,以树形或更复杂的数据结构来呈现。
1.2.5.2 测试执行结果
测试执行的数据包括一组测试用例的执行结果,这个结果一般会包含一次执行的所有信息,比如测试的版本、所用的测试用例,有多少测试用例执行通过、多少测试用例执行失败,等等。然后计算一个通过率,用来反应被测功能的质量。
对于自动化执行工程师来说,除了这些常规数据,他们还希望获取一个横向比较的结果,比如和前几次的版本比较,通过率的变化趋势,具体每个测试用例成功或失败的次数,甚至在不同的版本之间进行比较。有了这样的比较就能知道,一个测试用例在之前的几次运行中,是否执行未通过,或者之前执行通过了,但是某一次执行失败了,这样定位很容易出问题。
由于自动化测试的结果本身并不一定反应产品质量的实际情况,比如有些因为测试用例本身的问题产生了执行失败。所以测试工程师往往希望能对测试结果做二次处理,对执行失败的脚本填写分析报告,确定是脚本的问题、环境的问题,还是最终产品的问题。
1.2.5.3 测试报告
除了测试执行工程师会分析自动化测试结果,团队的管理者也会关心测试的结果,但是管理者所关心的可能并不是执行结果的细节,而是从数据和图表中获取直观的统计信息。
测试报告应该能够将所有的测试结果进行汇总,比如我们有十几套测试环境,在每个晚上测试不同的功能或不同的产品,这样第二天就会产生十几套测试结果。测试报告需要将这些结果进行汇总,生成图表,集中展示所有测试的结果。
1.2.6 与CI/CD的集成能力
所谓的CI/CD,其全称是持续集成(Continuous Integration)和持续交付(Continuous Delivery)(或持续部署(Continuous Deployment))。
一个团队的开发人员持续将软件代码提交到主干分支,就是一种持续集成(CI)的过程,但是这种过程必须依赖于自动化,包括自动化测试。团队必须使用持续集成工具来监控每一次的提交,并且对提交的代码进行自动化测试。
持续交付(或持续部署)是指,通过自动化地构建、测试和部署,快速循环交付产品。持续交付和持续部署在本质上没有太大的区别,因为所谓的持续交付就是指软件能够持续地发布状态,而持续部署是指,软件能够持续地自动部署到生产环境中。
一般来说,整个CI/CD的过程应该是全自动的,当代码修改被提交到主干分支后,持续集成工具就会对提交的代码进行检测,执行单元测试或可用性测试的自动化测试,以保证基本功能的稳定。
软件构件被编译打包后,能够自动运行回归测试,甚至上线交付测试,并自动部署产品。虽然并不是每个企业都需要持续部署(对外发布),但是我们依然可以使用持续集成的概念和部分持续交付或部署的概念,比如企业内部的发布版本。
我们知道,自动化测试在整个CI/CD流程中起着举足轻重的作用,所以自动化测试工具必须能和CI/CD工具方便地集成。比如,当持续集成工具触发了代码提交,自动化工具必须执行相应的测试用例,在保证代码的提交不会产生灾难性后果的前提下,报告持续集成工具的测试结果。同样,在软件包生成后,回归测试就应该被触发,自动对软件包进行回归测试,并产生测试结果和报告,从而决定软件包是否能够被交付或部署。
对自动化测试工具来说,除了能够人为地设置测试任务,还能提供一定的方法集成到CI/CD的工具中,通过工具来触发测试,并将测试结果返回给CI/CD的工具。
1.2.7 快速部署和较低的学习成本
虽然软件自动化测试有着诸多优势,但是任何一个工具,都有部署的成本和学习的成本。
对于一个初期的团队,可能因为时间原因需要快速建立起一个自动化测试平台,往往会有各种强耦合存在(比如与操作系统的耦合,与测试环境的耦合),这种强耦合会非常不利于部署,比如我们规定了环境变量的名字、配置文件存放的目录、日志文件存放的目录,规定了被测设备的连接地址,等等。当有新的工程师参与到项目中,并且准备再搭建一套测试环境的时候,如果没有很好的文档指引,根本不知道如何下手去配置一套新的测试环境。
测试工具所需要的依赖也是一个关键问题。即便能够做到去除与操作系统的耦合,以及与环境的耦合,有时依赖关系也会成为快速部署的阻碍。当然,我们可以交由开发工具来解决,比如Python的requirement文件,C#的NuGet等,但是作为自动化工具的开发者,必须要考虑有些并不存在于公共开源社区的包如何发布的问题。
一个完美的部署过程应该是不需要过多的额外配置的,比如我们安装操作系统,仅仅需要告诉安装程序安装在哪个磁盘,就可以进行无人值守安装,或者用一个漂亮的向导界面指引安装者一步一步填写配置信息。同样,如果将自动化测试工具看作一个软件,它也应该拥有这种能力。甚至在高度自动化的持续集成过程中,自动化测试工具能够通过容器部署,在测试完成之后随着容器而销毁。
工具的学习成本同样应该在团队里面被考量,因为随着团队规模的变化或人员的流动,新的工程师都需要进行工具使用或自动化测试用例开发的学习。除了需要完善的文档,还有工具本身的易用性和使用逻辑的设计是否合理都很重要。比如GUI界面总是比命令行显得更友好些,当由于配置错误导致测试用例无法执行时,需要有详细的提示告诉使用者哪里出错了,等等。