1.1 软件测试各个阶段的自动化需求
在软件项目中,不同的阶段会有不同的测试需求,因此产生了不同的测试类型。大多数团队会有如下几个阶段的测试:
单元测试:开发阶段,开发人员代码级别的测试。
功能测试:某个功能或特性完成后,测试人员对这个功能或特性进行的单独的测试。在这个阶段,一般功能不会相互影响,测试的关注点比较单一。
回归测试:对于已经实现的功能进行的测试,这些功能已经经过了一轮或多轮测试,回归测试用以保证这些功能的完整性。
可用性测试和冒烟测试:这里的可用性测试很多人称之为Sanity Test,可用性测试的目的主要是保证代码的提交不会对软件产生影响,而冒烟测试主要用于验证整个系统的关键功能是否正常。这两种测试经常会有人混淆,或者当作一回事来看待。这是因为,这两种测试的特点就是只运行关键的测试用例,以保证一些基本且重要的功能没有问题。
系统测试:系统测试是一个比较笼统的概念,通常很多团队会有系统测试部门对产品进行一系列的测试,比如端到端的测试、异常测试、压力测试、性能测试等。这种测试一般都是系统级别的,测试规模比较大,测试时间比较长,测试人员更容易脱离测试用例,根据自己的经验发现系统性的问题。
在1.1节中,我们会针对这些软件测试的阶段,分析它们对自动化测试工具的一些具体需求。
1.1.1 单元测试
我们对于单元测试的一般定义是对软件中的最小可测试单元进行的测试,最小可测试单元可以是一个函数、一个方法、一个类,或者一个很小的模块。单元测试往往由开发人员自己写测试代码来实现,对于一个函数、方法或者一个类,开发人员会设计一些测试用的数据,并且构造一段代码,执行并检查输出。比如一个矩形的描述类:
这个类的功能很简单,x和y代表矩形的边长,area方法返回矩形的面积,perimeter方法返回矩形的周长。作为开发人员的单元测试用例,一般会这么写:
这种方法看似没有什么问题,但是如果测试的内容比较多,比如这里只测试两个方法的正常执行情况,如果还要测试不同的输入、边界值、非法值、默认值,等等,这些用例就无法统一输出,可能十个测试人员有十种风格,如果团队强制开发人员在提交代码之前提供单元测试报告,那么这种方法显然不可取。
现在很多编程语言的开发工具包都包含了单元测试的模块,以帮助开发人员建立标准的单元测试模块。比如Python就提供了UnitTest的包,让开发者可以方便地构建单元测试用例。上面的例子如果使用UnitTest来构建,会是这样的:
UnitTest提供了统一的测试用例模块和执行模块,并且还有很多assert方法提供,使得开发人员可以统一构建单元测试用例,输出统一的测试报告。
可以看到,对于单元测试的自动化,已经有比较成熟的工具,但是对于开发者来说,他们更希望花心思在如何构建测试数据上,而不是测试用例设计本身,并且对于一些复杂的函数、类或模块,如何构造输入数据,自动生成Mock数据等,也是对单元测试工具的一个挑战。
1.1.2 功能测试
一般情况下,功能测试是测试人员开始介入的第一个阶段。开发人员将完成的功能特性写成文档交给测试人员,测试人员根据这些文档开发、设计测试用例,最后执行测试用例,对功能进行验证。
在理想情况下,如果功能特性的规范写得足够清晰,测试人员不仅能够在功能完成前开发完测试用例,甚至可以根据这些用例来开发自动化测试用例。虽然很多企业或团队都希望能够根据一些质量体系的规范,尽可能提供详尽的产品文档,但在大多数实践中,我们总会碰到这样那样的问题,基本可以归纳为以下几点:
(1)功能文档表述不规范,造成测试工程师理解偏差,导致设计的测试用例变成了无效测试用例。当然在大多数情况下,开发和测试人员会针对文档进行审阅(Review),尽可能避免这类问题的产生,但是一切都是在纸上谈兵的阶段,没有实际的操作,很难保证设计的测试用例能够真正适配和覆盖到相应的功能的测试。
(2)需求变更,导致功能特性更新,直接影响原先的测试用例。对于一些以客户为导向的项目团队,这个情况特别容易出现。比如,原本客户的下单功能只需要填写收货地址和收货要求,但是由于想提供更好的服务,于是增加了填写收货时间的选项。这样一来,在提交订单的功能点上,就需要加入对收货时间的验证,这就导致了测试用例的变更。
(3)新功能不稳定,导致测试用例无法顺利执行。虽然在大多数情况下,开发人员被要求做单元测试,但是当功能集成到整个系统中后,由于是多人协作,当大家的代码统一入库后,就可能会有很多意想不到的问题。有时这些问题是很严重的,会导致整个功能无法继续测试,或者需要一些特殊的操作来绕过问题,继续执行之后的测试。
对于这些情况,如果想要执行自动化测试,就会遇到一些麻烦。比如因为测试用例的设计问题导致测试执行过程不正确或者不完善,又或者针对需求变更导致的功能变化或配置过程变化,都会引起相应的自动化测试用例的修改,这无疑增加了本阶段测试人员的工作量。
而新功能不稳定,更会导致自动化测试遇到一定的阻碍。比如,一个严重问题导致系统的挂起,会阻塞所有接下来的执行用例,又或者一些无法预料的异常会影响部分测试用例的执行。这就使得自动化测试变得不那么自动,也就是说,需要测试工程师值守,在遇到一定问题之后手动介入去解决,即便能不断在测试用例里面加入分支代码来处理这种异常,也不是高效的手段。
如果我们把测试用例也看成软件开发,那么测试用例也是一个软件产品,其质量本身也需要一个完善的过程。在新功能阶段,测试用例也是新开发的,当遇到问题之后,会使测试用例本身的问题和产品的问题纠缠在一起,这会让问题的排查变得更复杂。
所以,很多团队的实践就是,在新功能测试中,自动化测试只是一个辅助的操作,用来覆盖一些简单的功能测试。比如接口测试,只需要验证接口的输入和输出,而这种自动化测试用例也相对容易开发和调试。对于复杂功能,基本还是依赖手工测试,只有到功能相对稳定的阶段,测试开发人员才会逐渐将测试用例转化为自动化测试用例。所以对于新功能测试,很多团队实际上还是会大量依赖手工测试。
可以看到,对于功能测试来讲,如果需要引入更多的自动化测试,那么测试用例本身的构建和修改一定要非常迅速,并且自动化测试工具要提供快速生成测试用例的能力,以此来应对需求的变更。而测试工具本身应该足够健壮和灵活,可以应对由于新功能的不稳定而产生的阻塞其他用例执行的情况,并且能够对灾难性的错误进行测试环境上的自动恢复。
1.1.3 回归测试
回归测试是软件开发迭代阶段中的一种测试,主要功能是保证原有的功能没有因为新功能的引入而遭到破坏。一般在新功能测试中,也包含了一定的回归测试。
自动化测试在这个阶段被大量用到。相较于新功能,回归测试主要是测试已经发布的或者已经稳定的功能,相应的测试用例已经相对稳定(注意相对这个词),自动化测试用例也经过多轮完善,执行也比较稳定。
对于单一的产品,即便新版本有改动,对回归测试用例而言也只要做些许修改即可。一切似乎很美好,但是现代软件发展太快,可能是为了应对技术的发展,也可能是为了应对市场的变化,一些公司推出一些形态功能类似,而配置方法、产品基准不同的产品。这些改动,如果作为新功能,就需要开发新的测试用例,如果复用老的测试用例,就要对测试用例进行代码的重构。
比如,为了加强用户的体验,某个网站的页面上,所有的输入日期的文本框都被新的日期控件所替代。这样原本的测试用例在抓取页面控件的时候就可能遇到问题,但其实用户的操作逻辑并没有发生大的变化。这种情况随着版本的更新和技术的进步,会变得越来越常见。
又比如某个芯片公司,原本一直在做EPON的网关产品,并且其符合中国的CTC标准,但是为了应对市场需求,又推出了GPON的网关产品。对于产品来说,仅仅是上联端口的硬件形态从EPON变成了GPON,但是其他功能几乎一模一样。你可以看看家里面用来上网的光纤调制解调器,很少有人会关心是EPON还是GPON的,因为其功能都是让你可以接入因特网。这些产品虽然硬件标准不同,协议不同,但是对于业务层面的测试需求,很多测试用例又是一致的。比如注册上线功能,虽然走不同的协议,但是上线业务的测试过程基本是一致的。
这些情况下,我想应该不会有团队考虑开发新的测试用例,而是想方设法去更新老的测试用例,以使它们能够应用于新产品的测试。
1.1.4 可用性测试及冒烟测试
可用性测试和冒烟测试都是一种快速验证的过程,其测试时间不会很长,甚至需要严格控制在一个范围内。所以对于这两种测试,应该完全通过自动化来覆盖。
为了能够保证这种快速验证,有些团队会有针对性地开发一些测试用例,但往往这些测试用例中的验证点,会和功能测试的测试用例的验证点重复,而不直接使用相应的功能测试的测试用例,目的就是缩短验证时间。因为功能测试的测试用例比较全面,测试数据比较多,执行的时间比较长。有些团队会在测试用例里面加入一些执行开关,让其在执行时,能够跳过某些验证点,达到测试用例复用的目的。
此外,这类测试需要能够灵活地部署,比如开发人员希望在代码入库之前或之后,能够迅速执行这样的测试用例,以保证入库代码基本功能的质量。这就会涉及一个测试用例选择和测试数据提供的问题——工程师如何快速选择合适的测试用例并生成测试集?这一切都会集成到整个团队的持续集成过程(CI)中,很多团队会使用Jenkins等工具,将可用性测试的执行放在编译的Job之后作为下游任务。当编译的构件生成后,自动执行下游的任务,即执行相关的测试用例。而对于一些持续部署或交付(CD),自动化又需要集成在发布上线前的节点,来保证上线的包不存在基本功能的问题。这些都对自动化测试工具的部署和执行方式提出了一定的要求。
1.1.5 系统测试
系统测试是一种比较复杂的测试过程。其主要目的是使被测产品的众多功能甚至是产品本身的集合,在以系统级别运行时进行行为的验证,其测试类型非常多样且复杂,主要体现在以下几个方面。
第一,相对于功能测试比较固定的测试环境,系统测试环境往往会比较复杂,配置繁多,并且不固定、不稳定。比如网络设备测试,功能测试会根据功能需求,建立一套相对固定的环境,规模也不会很大。但是作为系统测试,可能会是一个多台设备的复杂环境,并且其配置也会变得复杂,以此来测试不同功能的组合。
所以,对于自动化测试来说,环境的配置要足够灵活,能够应对不同的环境配置来执行相同的测试目的。比如WIFI MESH的测试,测试终端设备漫游,并不会限制WIFI AP数量的多少,可能有一个下限,比如AP不能少于3个,但是针对3个AP的环境和10个AP的环境,测试用例应该能够自动地匹配,甚至要做到足够智能,能够判断环境是否满足测试用例的需求。
第二,测试用例的容错性要足够强,系统测试的配置和测试过程的复杂度,对测试用例本身的稳定性,或者被测系统的稳定性提出了一定的需求。我们不希望一个历时3小时的测试用例,因为一个小问题导致整个测试用例的失败。而对于非法测试这类具有一定危险性的测试,我们同样希望,在被测系统进入一个不正常的状态时,测试工具能够有能力去恢复它。
第三,压力测试和性能测试的需求。对于标准的性能测试要求,我们希望自动化工具能够快速集成相应的标准性能测试设备或工具。比如,网络流量的一些诸如RFC2544的标准,能够快速地集成相应的流量测试设备。而对于一些非标准的性能或基线测试(比如网站的并发数量),能够快速地利用第三方工具(比如Jmeter)集成和开发相应的测试用例,并生成可读性较强的测试报告。
第四,客户场景模拟测试。即便有聪明的测试工程师设计种类繁多的测试用例,总会有他们想象不到的场景,而客户才是最佳的麻烦制造者。这里我们不分析产生这种情况的原因,但是客户确实会经常发现一些我们发现不了的问题。所以很多企业会花一些人力和物力来收集客户的场景,在系统测试中加入这样的场景模拟。但是测试用例一般都是设计好的,按照固定顺序或者一些简单的随机顺序执行,而客户的操作会更加随机而不可捉摸。如果客户发现了问题,在解决问题前,也需要对客户的场景进行模拟和简化来复现问题和定位。
可以看到,对于系统测试,自动化工具需要具有足够的弹性来匹配不同的测试场景,而不是通过不同的测试用例来对应不同的场景,同时要有足够强的扩展性,能够加入不同的辅助测试工具和设备来满足更多的测试需求,并且要有比较好的测试流程控制能力,能够快速地设计测试用例的工作流,应对客户提出的问题。