为什么你的Angular代码很难测试

Angular推出有好几年的时候了, 跟其他的MV*框架相比, 它的双向绑定, 无须显式声明Model, 模块管理, 依赖注入等特点都给Web应用开发带来了极大的便利, 另外, 借助于它众多强大的原生directive, 我们几乎可以避免麻烦的DOM操作了, 除了这些, Angular还有一个很大的亮点, 那就是高度的可测试性.

今天的Web开发已经不同往日, 更多的交互与逻辑都需要在前端完成, 有时候, 前端的代码量甚至在后端之上. 怎么去保证如此多的前端逻辑不被破坏, 依赖于功能测试? 这显示不现实, 功能测试很耗时, 而且它的创建成本较高, 所以通常只用它来覆盖最基本的那部分逻辑, 另一方面, 功能测试是依赖于流程的, 如果你想验证购买页面上的某个前端逻辑, 那么你就不得不一路从产品详情页面老老实实点过来, 反馈时间太长了, 可能你要等一分多钟才知道某个功能出错了, 我们自然不想把宝贵的开发时间浪费在等待上.

我在过去一段比较长的时候里都在项目上使用Angular, 在感受到Angular带来的便利的同时, 也饱受了Angular测试的折磨, 因为我一直觉得Angular的单元测试很难写, 跟JUnit + Mockito比起来, Angular代码的单元测试真是感觉写起来不得心应手, 更别说用TDD的方式来驱动开发.

我一直在思考为什么Angular社区说Angular的测试性很高, 但是在项目上实现用起来却是另一番境地. 经过分析项目上的代码, 我觉得要想驱动测试开发Angular代码, 那么其实是对你的Angular代码提出了比较高的要求, 你要遵循Angular的风格来开发你的应用, 只有你了解了其中的思想, 你的测试写起来才会轻松.

如果你已经使用Angular有一段时间了, 但是还没有读过这篇文章, 那么我强烈推荐你去读一下: Thinking in Angular

先来看一看怎么样的Angular代码才是苗正根红的Angular代码.

避免使用任何的DOM操作

像DOM操作这样的脏活累活都应该交给Angular的原生directive去做, 我们的Angular代码应该只处理与DOM无关的业务逻辑. 来看一个简单的例子, 我们想创建一个简单的邮箱地址验证的directive, 它要实现的功能是, 当焦点从邮箱地址输入框移出的时候, 对输入框中的邮箱地址进行验证, 如果验证失败, 则向输入框添加一个样式表示输入的地址不合法, 比较糟糕的实现可能是这样的

<label for=”email”>Your email:</label>
<input id=”email” type=”text” ng-model=”email” validate-on-blur/>

angular.module(“TheNG”, [])
  .directive(“validateOnBlur”, function() {
    return {
      link: function(scope, element, attrs) {
        var validate = function(email) {
          return email.indexOf(‘@gmail.com’) !== -1;
        };
        element.on(‘blur’, function() {
          if (!validate(scope.email)) {
            element.addClass(‘error - box’);
          }
        });
      }
   };
});

上面的代码应该可以满足我们的要求(验证逻辑因为不是我们关注的重点, 所以并不完善), 而且这个directive实现起来也挺简单的, 但是现在让我们一起来分析一下为什么我们认为这种写法是比较糟糕的.

最简单的办法就是在你的directive里面去找所有与DOM操作相关的代码.

首先看到的就是on()这个事件监听器. 完全没有必要自己去监听发生在被directive修饰的元素上的事件, angular有一整套的原生directive来干这个事情, 这里正确的做法应该是使用ng-blur来处理blur事件.

下一个有问题的地方就是addClass(), angular除了提供了事件监听相关的directive外, 也提供了操作元素本身属性的directive, ng-class就可以用来替换addClass()方法.

按照这个思路修改后的代码:

<label for=”email”>Your email:</label>
<input id=”email” type=”text” ng-model=”email” ng-blur=”validate()” ng-class=”{‘error-box’: !isValid}” validate-on-blur/>

angular.module(“TheNG”, [])
  .directive(“validateOnBlur”, function () {
    return {
      link: function (scope, element, attrs) {
        scope.isValid = true;
        scope.validate = function () {
        scope.isValid = scope.email.indexOf(‘@gmail.com’) !== -1;
      };
    }
   };
  }
);

比较一下这两个版本的实现, 是不是修改后的版本更简短, 更容易理解一些. 在新的版本里面, 我们只处理了业务逻辑, 即判断一个邮箱地址是否合法, 至于何时触发验证, 验证失败或成功之后应该有怎样的样式, 我们都统统交给了angular原生directive去处理了.

从测试的角度来看, 如果想给第一个版本的实现写单元测试, 那么要准备和验证的东西都很多, 我们需要设法去触发对应元素的blur事件, 然后再验证这个元素上是否添加了error-box这个class, 根据我的经验, 有时候为了验证这些DOM更新, 你还不得不创建真实的DOM结构添加到DOM tree上去, 又增加了一部分工作量.

而版本二就简单多了, 只定义了一个Model值isValid来标识当前的邮箱地址是否合法, validate()方法会在每次失焦之后自动执行, 要为它添加单元测试, 则只需要调用一下它的validate()方法, 然后验证isValid的值就可以了. SO EASY!~

将所有第三方服务封装成Service

一个Web项目中总是无法避免地要使用一些第三方的服务, 这里讨论的主要是前端的一些第三方服务, 比如在线客服, 站点统计等, 这些代码都在我们的控制之外, 大多数时候下都是从服务提供商的服务器上下载下来的, 而我们需要在业务代码中调用这些代码.如果我们每次都是赤裸裸地以全局变量的形式来使用这些服务, 那么造成的问题就是这样的代码很难测试, 因为这些代码是不存在于我们的代码库中的, 而且内容应该也是不定时更新的, 大多数情况很多人会因为这些原因放弃到对这类操作的测试.假设我们现在需要在某些动作发生之后调用一个第三方服务, 这个第三方服务叫做serviceLoadedFromExternal, 它提供了一个API叫做makeServiceCall, 如果直接使用这个API, 那么在测试中很难去验证这个服务被执行了(因为在单元测试环境中这个服务根本不存在), 但是如果我们将这个服务包装成一个angular service, 那么就可以在测试中轻易地将它替换成一个mock对象, 然后验证这个mock对象上的方法被调用了就可以了.比较下面的两段代码:

  • 直接使用第三方服务
angular.module(“TheNG”, [])
  .controller(“AController”, function ($scope) {
    $scope.someAction = function () {
    // handle some logic here
    serviceLoadedFromExternal.makeServiceCall();
   };
});
  • 使用封装成service的第三方服务
angular.module(“TheNG”, [])
  .factory(“wrappedService”, function () {
    return {
      makeServiceCall: function () {
        serviceLoadedFromExternal.makeServiceCall();
      }
    };
 })
 .controller(“AController”, function ($scope, wrappedService) {
   $scope.someAction = function () {
   // handle some logic here
   wrappedService.makeServiceCall();
   };
 });

Angular是高度模块化的, 它希望通过这种模块的形式来解决JS代码管理上的混乱, 并且使用依赖注入来自动装配, 这一点与Spring IOC很像, 带来的好处就是你的依赖是可以随意替换的, 这就极大的增加了代码的可测试性.

尽量将Ajax请求放到service中去做

Angular中使用service来组织那些可被复用的逻辑, 除此之外, 我们也可以将service理解为是对应一个领域对象的操作的集合, 因此, 通常会将一组Ajax操作放在一个service中去统一管理.

当然了,你也可以通过向你的directive或是controller中注入$http, 但是我个人不喜欢这种做法. 首先, $http是一个比较初级的依赖, 与其实注入的业务服务不是一个抽象层级, 如果在你的业务代码中直接操作http请求, 给人的一种感觉就像是在Spring MVC的request method中直接使用HttpServeletRequest一样, 有点突兀, 另外会让整个方法失衡, 因为这些操作的抽象层次是不一样的. 其次就是给测试带来的麻烦, 我们不得不使用$httpBackend来模拟一个HTTP请求的发送.

我们应该设法让测试更简单,通过将Ajax请求封装到service中, 我们只需要让被mock的service返回我们期望的结果就可以了. 只有这样大家才会喜欢写测试, 甚至是做到测试驱动开发, 要去mock$http这样的东西, 显然是增加了测试的负担.

使用Promise处理Ajax的返回值, 而不是传递回调函数

Angular中所有的Ajax请求默认都返回一个Promise对象, 不建议将处理Ajax返回值的逻辑通过回调函数的形式传递给发送http请求的service, 而应该是在调用service的地方利用返回的promise对象来决定如何处理.

让我们通过下面的例子来感受一下:

angular.module(“TheNG”, [])
  .service(“deliveryService”, function ($http) {
    this.validateAddress = function (address, success, failure) {
      $http.post(‘/unknown’, address).then(success, failure);
    };
  })
  .controller(“DeliveryController”, function ($scope, deliveryService) {
     var acceptAddress = function () {
       console.log(‘address accepted’);
     };
     var rejectAddress = function () {
       console.log(‘address rejected’);
     };
     $scope.validateAddress = function () {
     deliveryService.validateAddress($scope.address, acceptAddress, rejectAddress);
   };
});

这里的处理办法是将快递地址验证失败或成功之后的处理函数都传给了deliveryService, 当验证结果从服务器端返回之后, 相应的处理函数会被执行. 这做写法其实是比较常见的, 但是问题出在哪里呢?

其实, 作为一个service的接口, validateAddress应该只接收一个待验证的地址, 验证完成之后返回一个验证结果就可以了, 本来应该是一个很干净的接口, 我们之所以丑陋把对应的处理函数也传进去, 原因就在于这是一个异步的请求, 所以需要在发请求的时候就将对处理函数绑定上去.

你应该已经猜到了第二个问题我会说一说对它的测试, 通常来说, 如果一个service创建成本较高或是存在外部依赖/请求的话, 我们会将这个service mock掉, 通过让mocked service直接返回我们想要的结果来让我们只关注被验证的业务逻辑.我们回头看一下上面的实现, 如果我们把deliveryService的validateAddress()方法mock掉, 那么我们根本没有办法去验证acceptAddress()和rejectAddress()里面的逻辑!

所以, 如果你的处理函数是传递给service中的API的话, 那么你的测试其实就已经跟这个API的实现绑定了, 你只有去创建一个真实的service并且让它发送HTTP请求, 你的处理函数才会被执行到.

经过这一番折腾, 你一定要说, 这测试比实现代码难写多了. 正确的打开方式应该是这样的: service的API只需要返回promise, 对应的处理函数的绑定在这个返回的promise上, 这样我们只需要mock那个service的接口让它返回一个我们期望的promise, 然后控制promise的结果让对应的处理函数被执行:

angular.module(“TheNG”, [])
  .service(“deliveryService”, function ($http) {
    this.validateAddress = function (address) {
      return $http.post(‘/unknown’, address);
    };
  })
  .controller(“DeliveryController”, function ($scope, deliveryService) {
     var acceptAddress = function () {
       console.log(‘address accepted’);
     };
     var rejectAddress = function () {
       console.log(‘address rejected’);
     };
     $scope.validateAddress = function () {
       deliveryService.validateAddress($scope.address).then(acceptAddress, rejectAddress);
     };
 });

本来打算接下来介绍一下Angular代码的单元测试的各种模式的, 写着写着篇幅有点多了, 期待下一篇吧.

Share

快速搭建IE测试环境(Virtualbox+ievms)

IE下的测试

作为一个有追求的程序员,应该尽可能的远离Windows系统。不论从专业开发者的角度,还是仅仅作为最终用户从使用体验上来说,Windows都可以算是垃圾中的战斗机:没有shell响应极慢(比如从开机到可用需要多久,再对比一下Mac下的体验)、大部分操作都强依赖于鼠标,没有对应的快捷键、各类病毒等等。

但是,作为一个职业的程序员,又很难绕开Windows这个猥琐而又事实上很现实的存在,毕竟Windows在非专业市场上的占有率还是不容小觑的。一般而言,开发人员可以很轻松的使用现代的操作系统,编辑器,开发工具完成实际的业务需求,这部分工作很可能占整个交付工作的40%,但是又不得不在多个浏览器(IE的各个版本)中花费另外的60%。

既然很难抛开,那么我们就需要想办法简化对其的使用,比如将Windows隔离为一个纯粹的测试环境(不安装任何其他的软件,并且一旦感染病毒之后可以快速恢复)。

  1. 将Windows安装到虚拟机中
  2. 使用工具将诸如下载镜像,安装系统,安装特定版本的IE等操作简化为一条命令
  3. 可以很容易的创建一个干净,纯粹,稳定的Windows环境

ievms正是这样一个工具,它提供安装了各种版本IE的Windows操作系统的镜像,支持IE6到IE11。默认的,用户可以安装从IE6到IE11的所有镜像,但是很可能你无须所有的环境,ievms也提供对应的参数来确保只下载某一个。

不过对于一个团队来讲,可以安装所有的镜像到团队的某台公共的机器上,供所有人来进行跨IE浏览器的各个版本的测试。

这些虚拟机镜像都是虚拟磁盘vmdk文件,因此你需要先安装VirtualBox)。

安装ievms

安装ievms非常容易,只需要下载一个脚本即可:

1
$ curl -s https://raw.githubusercontent.com/xdissent/ievms/master/ievms.sh -L

github会将该请求重定向,所以加上-L参数来跳转到实际的地址。下载之后,执行该脚本:

1
2
$ chmod +x ievms.sh
$ ./ievms.sh

默认的ievms会下载所有的虚拟机镜像,可以通过参数IEVMS_VERSIONS来选择特定版本的虚拟机:

1
$ ./ievms.sh IEVMS_VERSIONS="7 8 9"

当然,也可以将这些命令合并为一行命令:

1
$ curl -s https://raw.githubusercontent.com/xdissent/ievms/master/ievms.sh -L | IEVMS_VERSIONS="7 8 9" bash

用法

安装之后,一个新的虚拟机会被添加到VirtualBox中,只需要启动这个虚拟机即可:

image

另外,在这个虚拟机中,可以很方便的连接到宿主机。比如在宿主机上的12306端口运行了某个Web应用,那么通过地址:http://10.0.2.2:12306 来访问这个应用。

注意: 由于是整个虚拟磁盘的形式发布,因此这些镜像的体积都非常大,所有的镜像安装之后,会占用37G的空间,对于任何一个开发机来说,这个尺寸过于庞大,但是对于整个团队来说,应该还是可以接受的。

官方给出的尺寸列表如下:

1
2
3
4
5
6
7
8
$ du -ch *
 11G    IE10 - Win7-disk1.vmdk
 11G    IE11 - Win7-disk1.vmdk
1.5G    IE6 - WinXP-disk1.vmdk
1.6G    IE7 - WinXP-disk1.vmdk
1.6G    IE8 - WinXP-disk1.vmdk
 11G    IE9 - Win7-disk1.vmdk
 37G    total

 

Share

说起BDD,你会想到什么?

706-林冰玉-说起BDD你会想到什么

在刚接触BDD(Behavior Driven Development,行为驱动开发)的时候,我以为就是用Cucumber这样的工具来编写场景用例,从而实现自动化测试,甚至很长时间分不清BDD和ATDD(Acceptance test driven development)到底有什么区别。那么,BDD真的就是用来做自动化测试的吗?本文就来跟大家分享一下我理解的BDD。

 

为什么要BDD?

开发软件系统最困难的部分就是准确说明开发什么” (“The hardest single part of building a software system is deciding precisely what to build” — No Silver Bullet, Fred Brooks) 。

场景一:业务分析人员觉得自己分析的需求已经写的很清晰了,并且跟技术人员进行了足够的沟通,可是开发完sign off的时候,发现所开发的功能还是跟期望有差距。

场景二:开发团队辛辛苦苦开发完一个功能,满怀信心的去给客户展示的时候,才发现原来客户需求的功能不是这样的。

这些场景是不是似曾相识?为什么会这样?第一个场景是开发团队内部技术人员跟需求分析人员的理解有偏差,导致大家理解的需求其实是不一样的;第二个场景是开发团队没有真正理解产品经理/客户所提出来的真实需求,导致开发的产品跟需求不一致。其实,产生这两个不一致的真正原因是因为不同角色有着不同的领域知识,说着不同的语言,大家在沟通的时候,如果都用自己领域语言,必然会产生沟通代沟,导致理解的不一致性。

领域知识不同、语言不通导致沟通障碍,这个客观存在的问题该如何解决呢?BDD正是为此而生。

 

BDD是什么?

BDD的提出者Dan North强调BDD不是关于测试的,它是在应用程序存在之前,写出用例与期望,从而描述应用程序的行为,并且促使在项目中的人们彼此互相沟通。

要给BDD下个清晰易懂的定义很难,包括大师们也这么认为,这里试着总结以下几点:

  1. 关注的是业务领域,而不是技术:BDD强调用领域特定语言(DSL, domain specific language)描述用户行为,定义业务需求,而不会关心系统的技术实现。
  2. 不是工具,强调的是一种协作方式:BDD要求各个角色共同参与系统行为的挖掘和定义,以实现对业务价值的一致理解。
  3. 不是关于测试的:BDD源自TDD,但重点不是关于测试,所强调的沟通与协作可以指导更好的做自动化测试。
  4. 全栈敏捷方法:BDD促使团队所有角色从需求到最后的测试验证,进行高度的协作和沟通,以交付最有价值的功能。

 

BDD怎么做?

用例场景的描述格式“GIVEN… WHEN… THEN… ”对大家都不陌生,但用这个格式写出好的用例却是非常的难,尤其是新手。这里总结几点供大家参考:

1.业务层抽取,业务语言描述

根据业务层的数据流,在每个数据停留点进行纵切,抽取出一个个用例场景。描述语言一定是业务领域可懂的,不要涉及任何实现相关的技术细节。所描述的场景一定是从业务层抽象出来,体现真实业务价值的。

2.技术人员可懂,自动化友好

所描述的用例场景要能驱动开发,必须要让技术人员易于理解;要指导自动化测试,还得要求对于自动化的实现是友好的。这一点似乎是跟第一点有些矛盾,但我们严格遵守BDD的格式要求还是可以做到的。其中,GIVEN从句描述的是场景的前提条件、初始状态,通常是一种现在完成时态;WHEN从句是采取某个动作或者是发生某个事件,一定是动词,通常是一般现在时;THEN从句用“应该…(should be…)”来描述一种期望的结果,而不用断言(assert),后者与测试关联更紧密。

3.数据驱动,需求实例化

抽象的业务语言描述的需求,往往由于太抽象而缺失掉很多关键信息,导致不同人员对需求理解的不一致。想要既抽象又能包含细节信息,就需要采用需求实例来描述。简单说来,就是给场景用例举例说明。举例就会需要列举数据,如果在场景用例描述里边直接添加数据实例,那样的用例将会很混乱,可读性和可维护性都非常差。如果我们能够在描述场景的用例里边用一些变量来代替,把变量对应的值(数据)提取出来存为一个表格或者独立的文件,这样将会使得用例的可读性很好,而且也不会缺失细节信息(数据),后期的维护和修改也较为方便。这就是数据驱动的方法来描述实例化的需求。

看几个例子,大家体会一下:

场景一:检查收件箱,可以看出第三个清晰明了且能体现业务价值,比较符合上面的要求。
Scenario: Check Inbox
  Given a user "Tom" with password "123"
  And a user "Jerry" with password "abc"
  And an email to "Tom" from "Jerry"
  When I sign in as "Tom" with password "123"
  Then I should see one email from "Jerry" in my inbox
Scenario: Check Inbox
  Given a user "Tom"
  And a user "Jerry"
  And an email to "Tom" from "Jerry"
  When I sign in as "Tom"
  Then I should see one email from "Jerry" in my inbox
Scenario: Check Inbox
  Given I have received an email from "Jerry"
  When I sign in
  Then I should see one email from "Jerry" in my inbox
场景二:限制非法用户查看某些受限内容,BDD强调什么(What),而不是怎么(How),第二个写的比较好。
Scenario: Redirect user to originally requested page after logging in
  Given a user "Tom" exists with password "123"
  And I am not logged in
  When I navigate to the home page
  Then I am redirected to the login form
  When I fill in "Username" with "tom"
  And I fill in "Password" with "123"
  And I press "Login"
  Then I should be on the home page
Scenario: Redirect user to originally requested page after logging in
  Given I am an unauthenticated user
  When I attempt to view some restricted content
  Then I am shown a login form
  When I authenticate with valid credentials
  Then I should be shown the restricted content
场景三:添加图书到购物车并计算总额
Scenario: Books add to shopping cart with correct number and total price
  Given a book "BDD" with price "30.5"
  And a book "Cucumber" with price "25.8"
  When I select "BDD"
  And I click the add to shopping cart button
  Then I should see one "BDD" in my shopping cart
  And the total price is "30.5"
  When I select "Cucumber"
  And I click the add to shopping cart button twice
  Then I should see two books "Cucumber" in my shopping cart
  And the total price is "82.1"
Scenario Outline: Books add to shopping cart with correct number and total price
  Given book <name1> with <price1>
  And book <name2> with <price2>
  When I add <number1> book <name1> and <number2> book <name2> to shopping cart
  Then I should see book <name1> and <name2> in my shopping cart
  And the total price should be <total>
  Examples:
  | name1    | price1 | number1 | name2    | price2 | number2 | total |
  | BDD      | 30.5   | 1       | -        | -      | -       | 30.5  |
  | Cucumber | 25.8   | 2       | -        | -      | -       | 51.6  |
  | BDD      | 30.5   | 1       | Cucumber | 25.8   | 2       | 82.1  |

BDD的工具有Cucumber、JBehave、Twist、Concordion等,工具的优缺点和使用方法,网上都有丰富的文档可参考,在此不作介绍。

BDD有什么好处?

BDD的作用是把利益关系人、交付团队等不同方面的项目相关人员集中到一起形成共同的理解共同的价值观以及共同的期望值。它可以帮助我们:

  • 关注用户行为
  • 交付最有用的功能
  • 在团队内部维护一致的术语
  • 探究需求实例
  • 编写和维护需求
  • 创建活的文档
  • 消除协作与沟通障碍

 

什么样的项目适合BDD?

  • 简单的一次性项目,沟通交流成本都较低的情况下,没有必要使用BDD;
  • 业务比较轻量,重在技术方面的项目,可以只使用TDD,或者简单的白板上的BDD,不需要在BDD工具记录需求用例文档;
  • 业务复杂、团队成员较多的项目,沟通成本高,BDD很有必要。

 

常见疑惑

1.BDD与TDD/ATDD

TDD是测试驱动开发,ATDD是验收测试驱动开发,都是关于测试的,是与所开发的系统紧密联系的。而BDD则不同,前面提到过BDD不是关于测试的,着重关注需求、关注客户的业务价值,所描述的需求用例是可以独立于软件系统存在的,因为客户的业务是始终存在的,不取决于是否有软件系统来支撑。

2. BDD与SBE

SBE(Specification By Example,实例化需求)是在BDD之后由Gojko提出来的,也是关于需求的,主要强调通过列举实例发现需求中的缺失概念。BDD也是关注需求的,同样会使用实例来描述行为。两者的本质没有区别,只是概念的差异。

Share

Selenium是一瓶陈年佳酿

WE_Logo_FullColor_Stack_copy

软件质量传道士——Anand Bagmar是一名具有丰富实际经验并以结果为导向的软件质量传道士,他在IT领域拥有超过17年的经验,其中有超过14年专注于测试领域。他热衷于交付一个高质量的产品,同时致力于自动化测试工具、架构和框架。Anand撰写测试相关的博客文章并开发了软件测试相关的开源工具——WAAT (Web Analytics Automation Testing Framework), TaaS (for automating the integration testing in disparate systems) 和 TTA(Test Trend Analyzer).

起源

敏捷方法论始于2001年。随着越来越多的团队开始理解到更多敏捷宣言之外的内容,他们也开始识别出真正敏捷所必需的不同实践。

测试自动化是团队在工作中做好敏捷的一个必要的实践。测试自动化看似简单,但实际上为了让它真正地具有实效,团队不仅需要在流程中遵守很多规则,而且需要选择既能提供快速反馈又能满足测试自动化扩展的合适工具和技术。

必须要说的是,即使到现在,在同一个迭代中,伴随开发工作完成全面的自动化测试,对很多团队来说都是一个巨大的挑战。如果测试自动化采用了那些难以适应被测产品快速改变的工具和技术,这种无力感就更强烈了。

2004年Jason Huggins在ThoughtWorks测试一个内部应用时,创建了一个JavaScript的测试工具;而这个工具改变了浏览器(基于浏览器的测试)自动化测试的方法。这个工具后来慢慢进化成开源的“Selenium”。如果感兴趣,可以参考“Selenium历史”了解更多的演变历史。

不光以前,现在依然有很多非开源的、商业的测试工具和产品,而这些工具的卖点都是在难以说明的复杂过程中自动地做自动化测试。很遗憾,想要使用这些工具和产品都需要付出非常高的授权使用费。

这样导致的结果就是,各种机构和团队为了控制成本,只允许一小部分人来使用,来限制这些产品的大规模使用。而这就进一步导致了团队间的隔离。

开源的Selenium挑战了这种工作方式,并且打破了团队间的藩篱。

所有知道如何编程的团队成员(QA,开发人员以及其他成员)现在都能为搭建和运行自动化测试贡献自己的力量,而不需要考虑授权的费用。

更重要的是,开发人员和QA现在能对测试实现有同样的认知,从而减少了做出错误假设的可能。

现在距离Selenium推出已经有十年了,这十年之间发生了很多事。在技术领域中,基于测试的工具和技术发生的变化更多。而Selenium不仅仅存活下来,,而且随着时间变大变强,这实属不易。

如果把Selenium比作一瓶酒,十年时间足够让它变成一瓶陈年佳酿吗?

Selenium目前在业界的声誉:

  • 现如今,对于做基于浏览器的测试的人来说,不了解或者说没听说过Selenium是相当罕见的了。
  • 目前有许多其他用不同语言实现的框架都是基于Selenium的,这为测试自动化提供了更多的可能。
  • 有许多具有创新力的公司都基于Selenium开发了不同的产品,为它们的客户提供了更多的价值,比如说SauceLabs和BrowserStack等。
  • 全球的服务型组织都会对潜在客户的进行管理层面的对话或者提供基于Selenium的服务。
  • 近年来涌现出大量组织来销售基于Selenium自动化的测试服务。
  • Selenium是求职的热门搜素技能之一(关于测试自动化)
  • Selenium团队也在努力快速地推出更多的功能,并且尽量标准化对浏览器和移动浏览器的自动化支持。

为什么Selenium如此流行呢?

所以,到底为什么Selenium已经存在了10年但还能越来越强大和流行呢?其实,原因很多,最重要的原因是——它就是简单有效,它能够实现API所宣称的能力。

它是开源的——所以你可以深入了解它的工作原理,如果有需要的话,社区的每个人都能力去改进它。而你不需要成为同类产品“白金级”用户,就可以接触到它的核心工作原理。

社区支持很强大。如果Selenium核心贡献者不能及时回答提问,社区里还有大量的专家可以提供支持。

很可能有人也遇到过类似的问题,所以思路/建议/变通的方法就有很多。除此之外,功能修复一般都很快。

它是免费的——所以只需要拥有良好的编程语言专业知识,不需要在工具上破费。

由于敏捷的广泛发展,基于Selenium的自动化满足了大多数团队对于测试自动化的需求。

Selenium是测试自动化的银弹吗?

和其他工具和技术一样,你需要在正确的场景下使用Selenium。要不然,你不仅会渐渐失去工作的价值,而且会面对更多的挑战。

随着敏捷方法论不断成为主流,对更好更快的测试自动化的需求也变得很明显。

其他原因之中有一条,Selenium IDE可以更好地让不了解Selenium具体如何工作的人,通过使用录制得到脚本和浏览器进行交互。可是,和其他录制的工具一样,你得保证录制的脚本在所有情况下都工作,而且你要能够智能地使用不同类型的数据(有需要的话),处理等待和超时。

遗憾的是,不少使用Selenium IDE来录制脚本的人开始自称Selenium专家。

基于Selenium的自动化看似是所有自动化测试问题的正确的解决方案。它开始成为自动化的银弹。可是,世界上并没有这样的银弹。我们忽略了测试金字塔有多层,每层代表的一种类型的测试都是待测产品有必要的。

理想的测试金字塔看起来的样子:

01

对于基于Selenium(也包括其他UI自动化测试工具)的测试,要聚焦于金字塔的顶部,而不是试图覆盖整个金字塔的各个层级。这也正是这类测试的真正价值所在。

通常,那些对于测试自动化框架应该如何构建完全没有概念,或者只有有限概念的人,成为了测试自动化的主管和测试架构师。更加糟糕的是,层层传递的压力和数量导向的测试以及测试自动化,导致只是快速地添加自动化测试,而并没有审慎地思考和规范的实践。

所以很快地,理想化测试金字塔变成了下述的反模式之一:

02

结果就是这颗银弹变成了一颗致命的子弹,导致团队对于自动化、自动化的结果和团队的成员都丧失了信心。

很多团队的情况都是:虽然有无数的基于Selenium的自动化测试,并且团队的绝大多数时间都耗费在维护和修复测试上,而且还会有专门的团队针对产品执行手动的回归测试。这无疑是一种双输的局面,但是却是非常真实和常见的现象。从而导致了团队中成员的相互指责。

至为重要的一点是,我们要构建鲜活的、并能随着被测产品用户案例不断进化的自动化测试框架 。自动化测试框架必须要可维护和可扩展,这是作为测试企业级产品的不言而喻的需求之一。同时,识别那些可以通过浏览器实现自动化的测试也是非常重要的。

那么,接下来会发生什么?对你来说意味着什么呢?

测试的未来非常有前景。每一分钟,我们都面临着设备过剩和技术发展;而对于那些没有走上学习和适应科学和技术趋势之路的人来说,这是非常可怕。

以下是一些可以让你沿着这种趋势前行的想法:

  • QA的职责变得更加复杂,并且需要思维和技术纪律的转换
  • 参与和理解产品架构,并且对其作出贡献;这有助于理解需要测试什么,需要在测试金字塔的哪一层级实现自动化以及为什么要这么做。
  • 了解测试自动化是软件开发的一种形式。精通开发实践的同时,应用测试技巧会帮助你做好测试自动化。
  • 网页测试主导了过去十年测试的发展,现在的QA不仅必须了解如何自动化地测试网页,还必须了解如何自动化地测试移动和交互性的应用。
  • 行为驱动测试(Behavior-Driven Testing,BDT)作为一种可以识别并构建有效的回归测试用例集的方法,可以帮助规避我们之前提到的那些反模式
Share

当谈论覆盖率时我们在谈什么?

80-20-Rule

代码覆盖率 vs. 测试覆盖率

代码覆盖率通常指跑完测试后, 由工具自动统计的在跑测试的过程中被测代码的覆盖率, 细分的话包括语句覆盖率, 分支覆盖率, 函数覆盖率等. 由于代码覆盖率可由工具自动产生, 采集成本非常低, 而又比较直观, 所以历来受到开发团队及管理者的欢迎, 有的组织甚至将其作为 KPI 指标之一.

然而围绕着代码覆盖率, 有很多有趣的事情, 尤其是将其作为 KPI 的时候. 你会发现, 长期在低位徘徊的代码覆盖率, 突然之间会有一个比较大的提升. 究其原因, 是开发团队在短时间内加了”测试”. “测试”是打引号的, 因为当我们近距离观察这些”测试”的时候, 会发现通常是调用了某个高层的入口函数, 因而牵出很多底层函数, 覆盖率就上去了, 然而, 没有一个断言(assertion), 或者是区区几个断言. 也就是说, 把产品跑了一遍, 但没有判断其行为是否符合预期, 而代码覆盖率突然就达标了.

尽管对于追求自我改进的团队来说, 不会这么掩耳盗铃, 代码覆盖率依然是有价值的反馈指标, 但这从侧面说明了代码覆盖率并没有表达出我们对于外部质量真正的关注点. 那么我们对于质量真正的关注点是什么呢?

是断言的覆盖率, 即测试覆盖率. 换句话说, 我们真正关心的是, 我们总共应该有多少测试用例/验收条件/检查点, 它们中有多少已经被覆盖了, 即做出了真正的断言. 但目前为止, 还没有工具能自动统计跑完测试后, 测试覆盖率是多少. 代码覆盖率仅仅是无法自动统计测试覆盖率时的一个替代品.

为了统计测试覆盖率, 需要准备分子和分母的信息. 分母是产品”完整”的测试用例列表, 分子是已经执行的测试用例列表, 包括手工和自动. 如果你关心测试覆盖率, 而手头又没有这两个东西, 就要开始准备了.

注1: 利用现有的 xUnit 测试框架, 可以在某种程度上得到测试覆盖率. 比如可以将”完整”的测试用例列表用 xUnit 的测试用例表达出来, 其中对于还没实现的, 设置为 ignore. 这样可以从最后的报告中看出总数, 和 ignore 的数量(当然如果你不做断言, 还是白搭). 现在更多的是借助管理工具甚至 Excel, 来手工维护”完整”的测试用例列表及状态. 如果你知道有更好的方式, 请告诉我.

注2: 前面”完整”的测试用例列表, “完整”一直打着引号, 因为这是一个无法证明的问题, 我们只能根据经验设计测试用例, 无法保证其完整性, 并且随着产品的开发, 这个列表也会动态更新. 至于如何让测试用例尽可能完整, 是组织应该投入的地方.

此测试覆盖率 vs. 彼测试覆盖率

基于前面的描述, 那么当我的测试覆盖率达到某个比较高的数值, 比如80%, 是不是我就可以比更低的数值比如20%, 对产品更有信心呢? 答案取决于你的测试用例的设计.

我们都听过80/20原则. 比如用户80%的时间在使用20%的功能, 20%的功能就可以支撑起用户最关键的业务场景. 那么, 如果80%的测试覆盖率, 覆盖的是那不常用的80%的功能, 而20%的覆盖率, 覆盖的恰恰是最常用最关键的那20%的功能, 那么, 你是否还像开始那样, 相信80%的覆盖率带来的安全感呢?

基于测试覆盖率很难达到100%这个前提, 基于我们的发布时间总是很紧张而又要保证质量这个前提, 我们必须投入精力, 做测试用例的价值分析, 挑选出最有价值的测试用例, 优先安排资源实现和运行.

如果团队的测试用例没有经过价值分析, 没有优先级划分, 那么这就是接下来马上应该做的事. 这牵扯到一个问题, 测试人员及测试技能的价值.

当我们谈论测试技能时我们在谈什么

最近几年随着自动化测试框架的流行, 评价一个人员测试能力的标准逐渐变成了是否能写自动化测试. 如果照这个标准, 所有的开发人员一夜之间都具备了合格的测试能力. 这显然是一个不成立的结论.

测试至少分测试用例的设计和测试用例的编写执行两部分. 自动化测试的长处仅仅在于编写执行. 使用自动化测试框架并不会自动让我们的测试更有效, 更完备, 更具洞察力. 而测试的有效性和完备性, 通常是我们更关注的. 然而遗憾的是, 通常组织中这方面的知识比较欠缺, 关注度不够, 技能交流较少.

如果我们交流测试知识时, 更多的是谈论 xUnit, RobotFramework等, 而不是等价类/边界值, 恶邻测试法/快递测试法, 关键路径分析等, 那几乎可以肯定我们遗漏了更重要的东西.

要在时间资源人力资源有限的情况下保证产品质量, 我们需要提高测试用例的设计能力, 价值分析能力, 安排合理的测试策略.

本文转自:http://liguanglei.name/blogs/2015/06/01/code-coverage-vs-test-coverage/

Share