【QCon】精华内容上线92%,全面覆盖“人工智能+”的典型案例!>>> 了解详情
写点什么

自动化单元测试实践之路

  • 2014-05-09
  • 本文字数:7842 字

    阅读完需:约 26 分钟

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

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

1 是否值得

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

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

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

TestPyramid ,如下图所示:

图 -1-1-TestPyramid

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

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

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

答案不言而喻了。

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

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

2 关键部分

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

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

配置管理:使用版本控制

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

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

一、 代码管理混乱。

二、 解决代码冲突困难。

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

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

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

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

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

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

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

  • 提交有意义的注释

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

refs #任务 id 提交说明

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

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

  • 所有的代码文件编码格式统一使用 UTF-8
  • 上班前更新代码,下班前提交代码

前一天,团队其他成员可能提交了许多代码到 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

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

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

3、统一软件构建阶段

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 将作为自动化单元测试持续集成的平台, 实现自动化构建。

图 -2-3-Jenkins 平台

代码质量管理平台:Sonar

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

主要特点:

  • 代码覆盖:通过单元测试,将会显示哪行代码被选中
  • 改善编码规则
  • 搜寻编码规则:按照名字,插件,激活级别和类别进行查询
  • 项目搜寻:按照项目的名字进行查询
  • 对比数据:比较同一张表中的任何测量的趋势

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

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

图 -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 流程

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

  1. 开发人员遵循每日构建原则,提交功能代码、测试代码(以 UnitTest 结尾的测试类)到 Svn;
  2. Jenkins 平台,根据配置原则(假设配置定时器每 6 分钟检查 Svn 有代码更新则构建)进行:代码更新、代码编译、UnitTest、持续反馈的流水线工作;
  3. 构建结果发送到 Sonar,并且把失败的构建以邮件方式通知影响代码的开发人员;
  4. 开发人员、测试人员需要在 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></br>
* O 中的中、英文业务名来自 BILLING_BUSINESS 表
*/
public List<businessvo> getBusinessesByIds(List<long> businessIds) { return billingBusinessDAO.getBusinessbyIds(businessIds); } } </long></businessvo>

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- 使用前后对比

增加单元测试之后:

  1. 开发效率有望提升 5-20%;重构、回归测试效率提升 10%,降低出错的几率,总体代
  2. 码质量提升;
  3. 在开发过程中暴露更多问题,将风险和压力提前释放,持续构建促使开发重视代码质量;
  4. UnitTest 质量对于团队来说,是可视化了,交付的是有质量的产品,而不是数量;

作者简介

李乐,测试经理,7 年以上工作经验,目前就职于 ChinaCache 质量部

博客:jooben.blog.51cto.com

微博:weibo.com/iamlile


感谢侯伯薇对本文的审校和策划

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2014-05-09 09:5618076

评论

发布
暂无评论
发现更多内容

Zepoch节点开放申请,Web3流支付巨头Zebec利好不断

股市老人

Web3流支付迎来新质变,Zebec开放Zepoch节点申请

EOSdreamer111

爬虫练习题(三)

张立梵

Python. 10月月更 爬虫案例

【LeetCode】字符串解码Java题解

Albert

LeetCode 10月月更

Web3流支付迎来新质变,Zebec开放Zepoch节点申请

小哈区块

Sentinel Go-毫秒级统计数据结构揭秘

柠檬汁Code(binbin0325)

数据结构 源码分析 限流 Sentine 10月月更

Surpass Day——Java static关键字、继承、方法覆盖

胖虎不秃头

Java 10月月更 se

Python进阶(四)浅谈Python闭包

No Silver Bullet

Python 闭包 10月月更

七天杀上GitHub榜首!Java并发编程深度解析实战,JUC底层原理揭秘

Geek_0c76c3

Java 数据库 开源 程序员 架构

版本控制 | 一文了解VR内容创作的步骤与关键技术

龙智—DevSecOps解决方案

vr VR/AR

【一Go到底】第八天---用户输入

指剑

Go golang 10月月更

关于 Angular view Query 的 id 选择器问题的单步调试

Jerry Wang

typescript 前端开发 angular web开发 10月月更

leetcode 106. Construct Binary Tree from Inorder and Postorder Traversal 从中序与后序遍历序列构造二叉树(中等)

okokabcd

LeetCode 算法与数据结构

Surpass Day——Java this关键字

胖虎不秃头

Java 10月月更 se

攻击面分析及应对实践

vivo互联网技术

安全 风险管理 互联网安全

直呼内行!阿里大佬离职带出内网专属“Redis设计应用实践”学习笔记

Geek_0c76c3

Java 数据库 开源 程序员 架构

静态代码分析 | 数字驾驶舱时代,如何确保车载信息娱乐系统的网络安全?

龙智—DevSecOps解决方案

网络安全 车载信息娱乐系统 IVI

golang反向代理实现中的坑位

有态度的马甲

Python进阶(三)函数式编程之reduce()

No Silver Bullet

Python reduce 10月月更

低代码将干掉65%软件开发工作,留给码农的时间不多了!

雨果

低代码

Windows开发工具安装

青柚1943

dapr

Qt | 按钮控件的使用 QCheckBox

YOLO.

qt 10月月更 C++

干货分享 | MatrixOne系统架构

MatrixOrigin

MatrixOrigin MatrixOne 金海

Docker远程连接设置

程序员欣宸

Docker 容器 10月月更

从SpringBoot启动,阅读源码设计

Java 架构

数据库基础

说故事的五公子

MySQL 数据库 sql

深入浅出MatrixOne Parser

MatrixOrigin

矩阵起源 MatirxOrigin MatirxOne

Qt | 实现网页历史记录和查找功能 QWebEngineView

YOLO.

qt 10月月更 C++

文盘Rust -- struct 中的生命周期

京东科技开发者

redis rust 生命周期 Trait Trait Objects

代码质量与安全 | 清洁代码(Clean Code)比您认为的更重要

龙智—DevSecOps解决方案

clean code 清洁代码

线下活动 | 龙智Atlassian ITSM 解决方案即将亮相2022全球运维大会上海站

龙智—DevSecOps解决方案

gops GOPS全球运维大会

自动化单元测试实践之路_最佳实践_乐少_InfoQ精选文章