先理一下自动化测试的概念,从广义上来说,一切通过工具(程序)的方式来代替或者辅助手工测试的行为都可以成为自动化。从狭义上来说,通过编写脚本的方式,模拟手工测试的过程,从而替代人工对系统的功能进行验证。
有赞是一家互联网行业的创业公司,测试起步较晚,发布非常频繁,就算每次只回归核心功能,对人数极少的几个测试人员来说工作量巨大,且基本是重复劳动,极其枯燥,持续时间长了也容易出错。
所以初期我们测试自动化切入的思路非常简单:从实际用户的角度出发,模拟真实的操作,替代现有的手工测试用例的执行。这样一来,每次重复的工作就可以用自动化来替代,测试人员只需要关注每次发布的增量需求即可。
随着脚本数量的增加,这种自动化覆盖的方式的弊端也逐渐暴露:
虽然我们在测试框架和工具层面通过结合selenium-grid实现了脚本并发执行和失败用例重试机制以提高执行效率和降低误报率,但是这种方式只能缓解问题,并不能从根本解决覆盖不全的问题。
正好赶上公司的SOA服务化进程,测试这边也开始配合的做自动化方面的转变,从原来的黑盒系统级自动化测试向分层自动化测试转变。
在谈分层测试之前,先回顾几个概念:
接下来我们谈谈有赞是如何随着系统拆分SOA服务化推进分层自动化测试的。先来看看经典的测试金字塔: 其中Unit代表单元测试,Service代表服务集成测试,UI代表页面级的系统测试。分层的自动化测试倡导产品的不同层次都需要自动化测试,这个金字塔也正表示不同层次需要投入的精力和工作量。下面我会逐层介绍有赞的分层自动化实践。
在系统拆分之前,有赞只有一个庞大的巨无霸系统,单元测试极度缺失。在系统逐渐SOA服务化的过程中,我们逐渐提出了对单元测试覆盖率的要求。
我们的单元测试会分别做DAO层和服务层的测试。DAO层的单元测试主要保障SQL脚本的正确性,在做服务层的单元测试时就可以以DAO层是正确的前提进行用例编写了。
为了做细粒度的测试,需要解决单元测试的外部依赖。系统和模块之间的依赖可以通过Mock框架(Mockito/EasyMock)解耦,同时可以结合h2database解决对数据库的依赖,使得测试用例尽可能做到可以随时随地运行。
这一层发现并解决问题付出的成本相对来说最低,自动化用例的维护成本也不高,总的来说自动化测试的投入产出比最高。
单元测试的责任主体一般来说是开发人员,写单元测试也是开发人员对自己的代码进行检查的一个过程。
我们在服务层的测试首要考虑的是各系统(子系统)的集成测试。因为在经过单元测试这一层的保障之后,在服务层我们更关注的是某个系统的输入输出功能是否正确,以及若干个系统间的交互是否和业务场景的要求一致。
先来看看我们系统拆分之后的SOA系统应用架构图:
这一层的被测对象是抽离了展现层的代码(前端以及部分后端展现层逻辑)。
鉴于有赞的测试起步较晚(应该很多创业公司都有类似情况),测试资源紧缺,代码覆盖率低得可怜。所以我们的初期自动化用例覆盖策略是这样的:
这样做的好处是,可以快速增加业务场景的覆盖面,同时事先准备好的API接口用例,可以作为系统拆分后的冒烟测试用例,起到核心老功能的回归作用(只是做系统拆分,业务逻辑以及对展现层暴露的接口行为不变)。毕竟在自动化测试的过程中,最怕的就是变化,会带来更多的脚本维护工作,而以这种方式覆盖的用例,目前来看维护成本很低。
再介绍一下这一层的初期我们用例的基本形态:
结合我们的交易系统举个例子:比如交易系统会依赖于商品和营销活动,那我们的下单场景的用例会依次调用商品和营销这几个系统的API构造数据作为用例的前置条件,然后按照下单的业务场景调用交易系统的下单接口,校验返回值以及写入DB的数据,最后做好数据清理的工作。
我们在这一层的测试框架选择是基于公司通用的服务框架(Nova)基础之上搭建的,架构图如下:
BIT :服务接口集成测试(Business Interface&Integration Test)
SUT:被测系统(System Under Test)
这一层的测试覆盖主要是由测试人员进行,是测试人员大展身手的地方。
我们不需要非常详细的了解代码的实现,但是我们的用例里充分体现了我们对系统的结构,模块之间关系等的充分的理解。
后续我们对于Service层自动化测试的推进策略是:
先提一个问题,既然在文章开篇提到了UI自动化测试有这么多弊端,这么劳民伤财,那么是否还有必要进行UI层的自动化呢?答案是肯定的,因为UI层是我们的产品最终呈现给用户的东西。所以在做好上面两层的测试覆盖之后,测试人员可以投入更多的精力到UI层的测试上。正是因为测试人员会在UI层投入较大精力,我们还是有必要通过自动化来帮助我们解放部分重复劳动力。
根据我们的UI层自动化实践,提一下我们的UI层自动化覆盖的原则:
我们提高UI脚本可维护性的方法是遵循Page Object设计模式。
Page Object模式是为了避免在测试代码中直接操作HTML元素,对Web页面的抽象。好处有:
以有赞首页的登录操作为例(Ruby):
class LoginPage include HeaderNav def login(account, password) text_account.wait_until_present.set(account) text_password.set(password) button_login.wait_until_present.click return MainPage.new(@browser) end private def text_account @browser.text_field(:name => 'account') end def text_password @browser.text_field(:name => 'password') end def button_login @browser.button(:class => 'login-btn') end end
下面我们来看看测试用例:
class TestLogin < Test::Unit::TestCase def testLogin @browser = Browser.new @browser.goto 'youzan.com' main_page = @browser.login_page.login('xx', '123') #断言 end end
这样最终的测试脚本呈现的就是单纯的页面操作逻辑,更贴近文本测试用例。
下面来看一下我们的测试框架:
随着服务层自动化覆盖率越来越高,UI层的自动化覆盖会逐渐转变为页面展示逻辑及界面前端与服务的展现层交互的集成验证。我们后续对于UI层自动化的演进规划是这样的:
UI层的自动化测试也是由测试人员负责,在覆盖了核心业务核心场景之后,不应该在这层的自动化覆盖上投入太多的精力和资源。就算我们在一定程度上提高了脚本的可维护性,可是毕竟自动化测试最怕的就是变化,而UI界面是变化频率最高的一层,所以还是得投入一定的精力维护,不是么?
有了上述各层的自动化测试脚本,下面我们需要建立起持续集成体系。 持续集成的目的:
我们的持续集成是基于Jenkins搭建的,主要的动作如下:
持续集成所需的支撑有:
对于持续集成我们后续的演进规划是朝着持续交付和持续部署的方向努力,在持续集成的基础之上,自动将代码部署到测试环境上方便测试人员进行手工测试。
本文主要从整体上介绍了在有赞SOA化的进程中,测试推行的分层自动化实践,以及后续的发展方向,同时简单介绍了相关的测试框架结构。下面再从整体回顾一下我们的分层自动化的要点:
至于各层投入的具体比重,还是需要根据项目的需求来实际规划。在《google 测试之道》一书,对于google产品,70%的投入为单元测试,20%为集成、接口测试,10% 为UI层的自动化测试。
最后再提一些观点吧: