【AICon】AI 基础设施、LLM运维、大模型训练与推理,一场会议,全方位涵盖! >>> 了解详情
写点什么

自动化单元测试实践之路

  • 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:5618114

评论

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

设计模式--享元模式

学Java的猪猪侠

一文搞懂 Flink Stream Join原理

shengjk1

flink源码 flink join

阿里混沌工程平台实践

心远

阿里巴巴 混沌工程

this指向

y

this指针

Vue中如何在线预览pdf文件

y

产品经理第 0 期训练营第九周作业提交

Krystal

2021金三银四总结面试必备清单:字节/蚂蚁金服/腾讯/百度

比伯

Java 程序员 架构 互联网 技术宅

一年增加 1.2w 星,Dapr 能否引领云原生中间件的未来?

阿里巴巴云原生

容器 微服务 云原生 k8s 中间件

浅论指针(二)

Integer

c 指针

基于docker部署jenkins(一)

李日盛

docker jenkins

行业首创,百度自主研发下一代区块链操作系统

CECBC

原子操作

一文搞懂 Flink 处理水印全过程

shengjk1

户口?大厂?高薪?生活?聊聊应届程序员的职业选择

流沙

职业发展

API 工具链研发的理论基础 - 导读

李宇飞

工具链 API sdk

systemedctl使用指南

happlyfox

3月日更

作业 - 第八章 数据分析

hao hao

产品 0 期 - 第八周作业

曾烧麦

产品训练营

继承

y

继承

用户路径地图

王一凡

关系数据理论是个什么牛马

学Java的猪猪侠

「产品经理训练营」第八章作业

Sòrγy_じò ぴé

操作系统--虚拟存储器概述

学Java的猪猪侠

关于全球央行数字货币实验的若干认识与思考

CECBC

银行

你有没有领导力?

石云升

领导力 28天写作 职场经验 管理经验 3月日更

K8s 原生 Serverless 实践:ASK 与 Knative

阿里巴巴云原生

Serverless 容器 云原生 k8s 存储

Flink SQL 自定义 Source format

shengjk1

flink sql

技术中台之DevOps动态表单体系构建

EAWorld

结合 Flink 学习装饰者模式

shengjk1

flink源码 flink源码分析

Android 系统开发做什么?

吴小龙同学

【操作系统】存储器管理

学Java的猪猪侠

携手百度智能云推动实现工业AR空间智能化

百度大脑

AR 百度智能云

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