自动化单元测试实践之路

info已经发布:
html

http://www.infoq.com/cn/articles/road-of-automated-unit-testing-practicesjava


自动化单元测试并非什么新鲜事物,它应该是团队锲而不舍的事情,可能有不少团队知道如何去作,可是还作得不够好;还有很多团队不知道如何去作,甚至有一些旧系统还不敢去重构,还在坚持着Java中的main方法调用的方式来执行,在漫长等待构建结果。mysql

本文主要讲基于Java项目如何作自动化单元测试的实践。web

1 是否值得spring

关于单元测试的意义,详细参考stackoverflow这篇文章:sql

http://stackoverflow.com/questions/67299/is-unit-testing-worth-the-effort数据库

Martin Fowler在博客(http://martinfowler.com/bliki/TestPyramid.html)中解释了apache

TestPyramid,以下图所示:tomcat

wps_clip_p_w_picpath-8211

图-1-1-TestPyramid服务器

Unit是整个金字塔的基石(在建筑行业,基石是作建筑物基础的石头),若是基石不稳,Service和UI何谈有构建意义呢?只有基石稳如磐石,上层建筑才够坚固。

原本想拿瑞士作钟表的例子来讲明下,但同事说的汽车例子更好。一辆汽车由许多配件组成,若是有如下两种选择,你会选择哪一个呢?

一、全部单元配件没有测试过,在4S店,销售人员告诉你:刚组装好,已经开了一天,能跑起来,你能够试试;

二、全部单元配件在生产过程已经通过严格测试,在4S点,销售人员告诉你,已经经过国家认证,出厂合格,有质量保证,你能够试试;

答案不言而喻了。

实施单元测试,并不表明你的生产效率能提升迅猛,反而有时候阻碍了瞬间的生产效率(传统的开发一个功能,看似就算完成的动做,增长单元测试看起来没法是浪费时间),可是,它最直接的是提高产品质量,从而提高市场的形象,间接才会提高生产效率。

作产品,究竟是要数量,仍是质量呢?这个应该留给老板们去回答,看企业是否须要长远立足。

2 关键部分

自动化单元测试有四个关键组成部分要作到统一,如图所示:

wps_clip_p_w_picpath-23863

图-2-1-关键组成部分

配置管理:使用版本控制

版本控制系统(源代码控制管理系统)是保存文件多个版本的一种机制。通常来讲,包括Subversion、Git在内的开源工具就能够知足绝大多数团队的需求。全部的版本控制系统都须要解决这样一个基础问题: 怎样让系统容许用户共享信息,而不会让他们因意外而互相干扰?

若是没有版本控制工具的协助,在开发中咱们常常会遇到下面的一些问题:

1、 代码管理混乱。

2、 解决代码冲突困难。

3、 在代码整合期间引入深层BUG。

4、 没法对代码的拥有者进行权限控制。

5、 项目不一样版本发布困难。

? 对全部内容都进行版本控制

版本控制不只仅针对源代码,每一个与所开发的软件相关的产物都应该被置于版本控制下,应当包括:源代码、测试代码、数据库脚本、构建和部署脚本、文档、web容器(tomcat的配置)所用的配置文件等。

? 保证频繁提交可靠代码到主干

频繁提交可靠、有质量保证的代码(编译经过是最基本要求),可以轻松回滚到最近可靠的版本,代码提交以后可以触发持续集成构建,及时获得反馈。

? 提交有意义的注释

强制要求团队成员使用有意义注释,甚至能够关联相关开发任务的缘由是:当构建失败后,你知道是谁破坏了构建,找到可能的缘由及定位缺陷位置。这些附加信息,能够缩短咱们修复缺陷的时间。示例:团队使用了svn和redmine,注释是:

refs #任务id 提交说明

每一个任务下能够看到屡次提交记录:

wps_clip_p_w_picpath-12436

图-2-2-相关修订版本

? 全部的代码文件编码格式统一使用UTF-8

? 上班前更新代码,下班前提交代码

前一天,团队其余成员可能提交了许多代码到svn,开始新的一天工做是,务必更新到最新版本,及时发现问题(例如代码冲突)并解决;

当日事,当日毕,下班别把当天的编码成果仅保存在本地,应当提交到svn,第二天团队更新就能够获取到最新版本,造成良性循环。

构建管理:使用Maven构建工具

Maven是基于项目对象模型(POM),经过为Java项目的代码组织结构定义描述信息来管理项目的构建、报告和文档的软件项目管理工具。使用“惯例胜于配置”(convention over configuration)的原则,只要项目按照Maven制定的方式进行组织,它就几乎能用一条命令执行全部的构建、部署、测试等任务,却不用写不少行的XML(消除Ant文件中大量的样板文件)。

或许,使用Ant来构建的团队要问,为何用Maven呢?简单来讲两点

一、对第三方依赖库进行统一的版本管理

说实话,ant处理依赖包之间的冲突问题,仍是得靠人工解决,这个对于研发来讲是消耗时间的,倒不如把节省的时间投入到业务中去。另外不再用每一个项目繁琐复制spring.jar了,经过maven自动管理Java库和项目间的依赖,打包的时候会将全部jar复制到WEB- INF/lib/目录下。

二、统一项目的目录结构。

官方的约定:http://maven.apache.org/guides/introduction/introduction-to-the-standard-directory-layout.html

src/main/java



Application/Library sources



src/main/resources



Application/Library resources



src/main/filters



Resource filter files



src/main/config



Configuration files



src/main/scripts



Application/Library scripts



src/main/webapp



Web application sources



src/test/java



Test sources



src/test/resources



Test resources



src/test/filters



Test resource filter files



src/it



Integration Tests (primarily for plugins)



src/assembly



Assembly descriptors



src/site



Site



LICENSE.txt



Project's license



NOTICE.txt



Notices and attributions required by libraries that the project depends on



README.txt



Project's readme



保证全部项目的目录结构在任何服务器上都是同样的,每一个目录起什么做用都很清楚明了。

三、统一软件构建阶段

http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html

Maven2把软件开发的过程划分红了几个经典阶段,好比你先要生成一些java代码,再把这些代码复制到特定位置,而后编译代码,复制须要放到classpath下的资源,再进行单元测试,单元测试都经过了才能进行打包,发布。

测试框架:JUnit&Mockito

? JUnit

JUnit是一个Java语言的单元测试框架。

2013年见过一个旧项目,测试代码仍是以main做为入口,为何要使用JUnit?

JUnit 的优势是整个测试过程无人值守,开发无须在线参与和判断最终结果是否正确,能够很容易地一次性运行多个测试,使得开发更加关注测试逻辑的编写,而不是增长构建维护时间。

团队示例代码:

// 功能代码

package com.chinacache.portal.service;

public class ReportService {

   public boolean validateParams() {

   }

   public String sendReport(Long id) {

   }

   public String sendReport(Long id, Date time) {

   }

}

// 单元测试代码

package com.chinacache.portal.service; // 必须与功能代码使用相同 package

public class ReportServiceUnitTest { // 测试类名以 UnitTest (单元测试) 或 InteTest (集成测试) 结尾

   // 测试方法名以 test 开头,而后接对应的功能方法名称

   @Test

   public void testValidateParams() {

   }

   // 若是功能方法存在重载,则再接上参数类型

   @Test

   public void testSendReportLong() {

   }

   // 若是一个功能方法对应多个测试方法,不一样测试方法可以使用简洁而又有含义的单词结尾,例如 success、fail 等    

   @Test

   public void testSendReportLongDateSuccess() {

   }

   // 这样经过测试方法名便可知道:测的是哪一个功能方法,哪一种状况

   @Test

   public void testSendReportLongDateFail() {

   }

}

? Mockito

Mockito是一个针对Java的mocking框架。使用它能够写出干净漂亮的测试用例和简单的API。它与EasyMock和jMock很类似,经过在执行后校验什么已经被调用,消除了对指望行为(expectations)的须要,改变其余mocking库以“记录-回放”(这会致使代码丑陋)的测试流程,使得自身的语法更像天然语言。

Mockito示例:

List mock = mock(List.class);

when(mock.get(0)).thenReturn("one");

when(mock.get(1)).thenReturn("two");

someCodeThatInteractsWithMock();

verify(mock).clear();

EasyMock示例:

List mock = createNiceMock(List.class);

expect(mock.get(0)).andStubReturn("one");

expect(mock.get(1)).andStubReturn("two");

mock.clear();

replay(mock);

someCodeThatInteractsWithMock();

verify(mock);

官方对比文章:http://code.google.com/p/mockito/wiki/MockitoVSEasyMock

反馈平台:Jenkins&Sonar

持续集成平台:Jenkins

Jenkins 的前身是 Hudson 是一个可扩展的持续集成引擎,主要用于:

? 持续、自动地构建测试软件项目

? 监控一些定时执行的任务

Jenkins将做为自动化单元测试持续集成的平台,实现自动化构建。

wps_clip_p_w_picpath-31307

图-2-3-Jenkins平台

代码质量管理平台:Sonar

Sonar (SonarQube)是一个开源平台,用于管理源代码的质量。Sonar 不仅是一个质量数据报告工具,更是代码质量管理平台。支持的语言包括:Java、PHP、C#、C、Cobol、PL/SQL、Flex 等。

主要特色:

? 代码覆盖:经过单元测试,将会显示哪行代码被选中

? 改善编码规则

? 搜寻编码规则:按照名字,插件,激活级别和类别进行查询

? 项目搜寻:按照项目的名字进行查询

? 对比数据:比较同一张表中的任何测量的趋势

Sonar将做为自动化单元测试反馈报告统一展示平台,包括:

单元测试覆盖率、成功率、代码注释、代码复杂度等度量数据的展示。

wps_clip_p_w_picpath-25900

图-2-4 Sonar平台

3 原则

自动化测试金字塔,也称为自动化分层测试,Unit是整个金字塔的基石,最重要特色是运行速度很是快;第二个重要特色是UT应覆盖代码库的大部分,可以肯定一旦UT经过后,应用程序就能正常工做。

Unit:70%,大部分自动化实现,用于验证一个单独函数或独立功能模块的代码;

Service:20%,涉及两个或两个以上,甚至更多模块之间交互的集成测试;

UI:10%,覆盖三个或以上的功能模块,真实用户场景和数据的验收测试;

这里仅仅列举了每一个层次的百分比,实际要根据团队的方向来作调整。

自动化单元测试原则

提交代码、运行测试的重点是什么?快速捕获那些因修改向系统中引入的最多见错误,并通知开发人员,以便他们能快速修复他们。提交阶段提供反馈的价值在于,对它的投入可让系统高效且更快地工做。

? 隔离UI操做

UI应看成为更高层次的测试Level,须要花费大量时间准备数据,业务逻辑复杂,过早进入UI阶段,容易分散开发的单元测试精力。

? 隔离数据库以及文件读写网络开销等操做

自动化测试中若是须要将结果写入数据库,而后再验证改结果是否被正确写入,这种验证方法简单、容易理解,可是它不是一个高效的方法。这个应当从集成测试的Level去解决。

首先:与数据库的交互,是漫长的,甚至有可能要投入维护数据库的时间,那将成为快速测试的一个障碍,开发人员不能获得及时有效的反馈。假设,我须要花费一个小时,才能验证完毕与数据库交互的结果,这种等待是多么漫长呀。

其次,数据管理须要成本,从数据的筛选(线上数据多是T级)到测试环境的M级别,如何把筛选合适的大小,这都使得管理成本增长(固然在集成测试中可使用DBUnit来解决部分问题)。

最后,若是必定要有读写操做才能完成的测试,也要反思代码的可测试性作的如何?是否须要重构。

单元测试决不要依赖于数据库以及文件系统、网络开销等一切外部依赖。

? 使用Mock替身与Spring容器隔离

若是在单元测试中,还须要启动Spring容器进行依赖注入、加载依赖的WebService等,这个过程是至关消耗时间的。

可使用模拟工具集:Mockito、EasyMock、JMock等来解决,研发团队主要是基于Mockito的实践。与须要组装全部的依赖和状态相比,使用模拟技术的测试运行起来一般是很是快,这样子开发人员在提交代码以后,能够在持续集成平台快速获得反馈。

? 设计简单的测试

明肯定义方法:

成功:public void testSendReportLongDateSuccess()

失败:public void testSendReportLongDateFail(),能够包括异常

和单一的断言,避免在一个方法内使用多个复杂断言,这会形成代码结构的复杂,使得测试的复杂性提升。

? 定义测试套件的运行时间

使用Mock构建的单元测试,每一个方法的构建时间应该是毫秒级别,整个类是秒级别,理想的是总体构建时间控制在5分钟之内,若是超过怎么办呢?

首先,拆分红多个套件,在多台机器上并行执行这些套件;

其次,重构那些运行时间比较长且不常常失败的测试类;

更多参考推荐阅读:《Unit Testing Guidelines》

http://geosoft.no/development/unittesting.html

4 流程

wps_clip_p_w_picpath-21033

图-4-1-典型工做流程

一、开发人员遵循每日构建原则,提交功能代码、测试代码(以UnitTest结尾的测试类)到Svn;

二、Jenkins平台,根据配置原则(假设配置定时器每6分钟检查Svn有代码更新则构建)进行:代码更新、代码编译、UnitTest、持续反馈的流水线工做;

三、构建结果发送到Sonar,而且把失败的构建以邮件方式通知影响代码的开发人员;

四、开发人员、测试人员须要在Sonar平台进行review;

5 实践

? Jenkins配置重点

ü 构建触发器:推荐使用PollSCM

Poll SCM:定时检查源码变动(根据SCM软件的版本号),若是有更新就执行checkout。

Build periodically:周期进行项目构建(它不care源码是否发生变化)。

配置时间:H/6 * * * *

ü Build配置

Goals and options:emma:emma  -Dtest=*UnitTest soanr:sonar

注明:

emma:emma,Add the "emma:emma" goal to your build to generate Emma reports;

-Dtest=*UnitTest,参数配置,运行以UnitTest结尾的测试类;

sonar:sonar,来触发静态代码分析。

须要安装Emma Plugin(https://wiki.jenkins-ci.org/display/JENKINS/Emma+Plugin)

ü 构建后操做

增长Aggregate downstream test results,勾选自动整合全部的downstream测试;

增长Editable Email Notification,在“高级”选项增长触发器“Unstable”,

勾选“Send To Committers”,Check this checkbox to send the email to anyone who  checked in code for the last build。

注明:Editable Email Notification插件是 https://wiki.jenkins-ci.org/display/JENKINS/Email-ext+plugin

另一些Jenkins的单元测试覆盖率展示方式,能够查看官网。

? 构建管理工具(Maven)

ü 项目统一使用Maven进行构建管理,在pom.xml中进行依赖jar包配置

ü 持续集成服务器上同时须要安装Maven,setting.xml除了配置仓库以外,还须要配置sonar,包括sonar服务器地址、数据库链接方式:

<profile>

   <id>sonar</id>

   <activation>

   <activeByDefault>true</activeByDefault>

   </activation>

   <properties>

   <!-- EXAMPLE FOR MYSQL -->

   <sonar.jdbc.url>

     jdbc:mysql://127.0.0.1:3306/sonar?useUnicode=true&characterEncoding=utf8

   </sonar.jdbc.url>

   <sonar.jdbc.driverClassName>com.mysql.jdbc.Driver</sonar.jdbc.driverClassName>

   <sonar.jdbc.username>sonar</sonar.jdbc.username>

   <sonar.jdbc.password>sonar</sonar.jdbc.password>

   <!-- SERVER ON A REMOTE HOST -->

   <sonar.host.url>http:/127.0.0.1:9000</sonar.host.url>

   </properties>

</profile>

? Mockito配置重点

全部单元测试继承MockitoTestContext父类

MockitoTestContext 父类:

package com.chinacache.portal;

import java.util.Locale;

import org.junit.BeforeClass;

import org.mockito.MockitoAnnotations;

import org.springframework.mock.web.MockHttpServletRequest;

import org.springframework.web.context.request.RequestContextHolder;

import org.springframework.web.context.request.ServletRequestAttributes;

import com.chinacache.portal.web.util.SessionUtil;

import com.opensymphony.xwork2.util.LocalizedTextUtil;

/**

* Mockito 测试环境。继承该类后,Mockito 的相关注解 (@Mock, @InjectMocks, ...) 就能生效

*/

public class MockitoTestContext {    

   public MockitoTestContext() {

       MockitoAnnotations.initMocks(this);

   }

}

BillingBusinessManager 源码:

package com.chinacache.portal.service.billing;

//引入包忽略...

/**

* 计费业务相关的业务方法

*/

@Transactional

public class BillingBusinessManager {

   private static final Log log = LogFactory.getLog(BillingBusinessManager.class);

   @Autowired

   private UserDAO userDAO;

   @Autowired

   private BillingBusinessDAO billingBusinessDAO;

   @Autowired

   private BillingBusinessSubscriptionDAO billingBusinessSubscriptionDAO;

   @Autowired

   private BillingBusinessSubscriptionDetailDAO billingBusinessSubscriptionDetailDAO;

   @Autowired

   private BillingRegionSubscriptionDAO billingRegionSubscriptionDAO;

   @Autowired

   private BillingRegionDAO billingRegionDAO;

   @Autowired

   private ContractTimeManager contractTimeManager;

   /**

    * 根据id查询业务信息

    * @return 若是参数为空或者查询不到数据,返回空列表<br>

    * O 中的中、英文业务名来自 BILLING_BUSINESS 表

    */

   public List<BusinessVO> getBusinessesByIds(List<Long> businessIds) {

       return billingBusinessDAO.getBusinessbyIds(businessIds);

}

}

BillingBusinessManagerUnitTest类:

//引入包忽略...

public class BillingBusinessManagerUnitTest extends MockitoTestContext {

   @InjectMocks

   private BillingBusinessManager sv;

   @Mock

   private BillingBusinessDAO billingBusinessDAO;

   @Test

   public void testGetBusinessesByIds() {

       List<BusinessVO> expected = ListUtil.toList(new BusinessVO(1l, "a", "b"));

       //简洁的语法以下所示

       when(billingBusinessDAO.getBusinessbyIds(anyListOf(Long.class))).thenReturn(expected);

       List<Long> businessIds = ListUtil.toList(TestConstants.BUSINESS_ID_HTTP_WEB_CACHE);

       List<BusinessVO> actual = sv.getBusinessesByIds(businessIds);

       Assert.assertEquals(expected, actual);

   }

  }

更多Mockito的使用,能够参考官网:http://code.google.com/p/mockito/

6 总结

如何增强开发过程当中的自测环节,一直都是个头痛的问题,开发的代码质量究竟如何?模块之间的质量究竟如何?回归测试的效率如何?重构以后,如何快速验证模块的有效性?

这些在没有作自动化单元测试以前,都是难以考究的问题。惟有经过数据去衡量,横向对比多个版本的构建分析结果,才可以发现整个项目质量的趋势,是提高了,仍是降低了,这样开发、测试人员才可以有信心作出恰当的判断。

固然,单元测试也不是银弹,即使项目的覆盖率达到100%,也不能代表产品质量没有任何问题,不会产生任何缺陷。重点在于确保单元测试环节的实施,能够提早释放压力、风险、暴露问题等多个方面,改变以往没有单元测试,全部问题都集中到最后爆发的弊端。

最后,用一张图来作个对比:

wps_clip_p_w_picpath-12612

图-6-1-使用先后对比

增长单元测试以后:

一、开发效率有望提高5-20%;重构、回归测试效率提高10%,下降出错的概率,整体代

码质量提高;

二、在开发过程当中暴露更多问题,将风险和压力提早释放,持续构建促使开发重视代码质量;

三、UnitTest质量对于团队来讲,是可视化了,交付的是有质量的产品,而不是数量;

相关文章
相关标签/搜索