您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  要资料 文章 文库 Lib 视频 Code iProcess 课程 认证 咨询 工具 讲座吧   成长之路  
会员   
 
   
 
  
每天15篇文章
不仅获得谋生技能
更可以追随信仰
 
 
     
   
 订阅
  捐助
自动化单元测试实践之路
 
作者 李乐,火龙果软件    发布于 2014-07-28
来自于要资料   2041 次浏览     评价:      
 

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

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

1 是否值得

TestPyramid,如下图所示:

图-1-1-TestPyramid

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

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

所有单元配件没有测试过,在4S店,销售人员告诉你:刚组装好,已经开了一天,能跑起来,你可以试试;
所有单元配件在生产过程已经经过严格测试,在4S点,销售人员告诉你,已经通过国家认证,出厂合格,有质量保证,你可以试试;
答案不言而喻了。

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

做产品,到底是要数量,还是质量呢?这个应该留给老板们去回答,看企业是否需要长远立足。

2 关键部分

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

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

配置管理:使用版本控制

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

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

一、 代码管理混乱。

二、 解决代码冲突困难。

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

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

五、 项目不同版本发布困难。

对所有内容都进行版本控制

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

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

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

提交有意义的注释

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

refs #任务id 提交说明

每个任务下可以看到多次提交记录:

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

1.所有的代码文件编码格式统一使用UTF-8

2.上班前更新代码,下班前提交代码

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

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

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

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

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

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

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

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

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

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

3、统一软件构建阶段

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

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

测试框架:JUnit&Mockito

1、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() {
}
}

2、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 是一个可扩展的持续集成引擎,主要用于:

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

2.监控一些定时执行的任务

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

图-2-3-Jenkins平台

代码质量管理平台:Sonar

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

主要特点:

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

2.改善编码规则

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

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

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

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

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

图-2-4 Sonar平台

3 原则

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

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

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

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

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

自动化单元测试原则

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

1、隔离UI操作

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

2、隔离数据库以及文件读写网络开销等操作

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

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

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

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

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

3、使用Mock替身与Spring容器隔离

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

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

4、设计简单的测试

明确定义方法:

成功:public void testSendReportLongDateSuccess()

失败:public void testSendReportLongDateFail(),可以包括异常

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

5、定义测试套件的运行时间

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

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

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

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

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

4 流程

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

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

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

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

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

5 实践

Jenkins配置重点

构建触发器:推荐使用PollSCM

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

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

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

1、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)

2、构建后操作
增加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)

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

2、持续集成服务器上同时需要安装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 如果参数为空或者查询不到数据,返回空列表

* O 中的中、英文业务名来自 BILLING_BUSINESS 表
*/
public List getBusinessesByIds(List 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);
}
}

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%,也不能表明产品质量没有任何问题,不会产生任何缺陷。重点在于确保单元测试环节的实施,可以提前释放压力、风险、暴露问题等多个方面,改变以往没有单元测试,所有问题都集中到最后爆发的弊端。

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

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

增加单元测试之后:

开发效率有望提升5-20%;重构、回归测试效率提升10%,降低出错的几率,总体代码质量提升;

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

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

   
 订阅
  捐助
相关文章

性能测试十问:测试经理篇
web前端性能优化进阶路
性能测试综述
VS2010中自动化测试—Web性能测试
相关文档

性能测试
性能测试执行之测试脚本录制
性能测试进阶1-流程篇
性能测试进阶2-方案篇
相关课程

性能测试方法与技术
使用LoadRunner进行性能测试
Android应用的性能测试
基于Android的单元、性能测试
 

LoadRunner性能测试基础
软件测试结果分析和质量报告
面向对象软件测试技术研究
设计测试用例的四条原则
功能测试中故障模型的建立
性能测试综述
更多...   


性能测试方法与技术
测试过程与团队管理
LoadRunner进行性能测试
WEB应用的软件测试
手机软件测试
白盒测试方法与技术

相关咨询服务
建立软件测试规范
性能评测与调优


某博彩行业 数据库自动化测试
IT服务商 Web安全测试
IT服务商 自动化测试框架
海航股份 单元测试、重构
测试需求分析与测试用例分析
互联网web测试方法与实践
基于Selenium的Web自动化测试
更多...   
 
 
 
 
 
每天2个文档/视频
扫描微信二维码订阅
订阅技术月刊
获得每月300个技术资源
 
 

关于我们 | 联系我们 | 京ICP备10020922号 京公海网安备110108001071号