网上看过一句话,单元测试就像早睡早起,每个人都说好,但是很少有人做到。从这么多年的项目经历亲身证明,是真的。

这次借着项目内实施单元测试的机会,记录实施的过程和一些总结经验。

项目情况

首先是背景,项目是一个较大型的项目,多个团队协作开发,采用的是SpringCloud作为基础微服务的架构,中间件涉及Redis,MySQL,MQ等等。新的起点开始起步,团队中讨论期望能够利用单元测试来提高代码质量。单元测试的优点很多,但是我觉得最终最终的目标就是质量,单元测试代码如果最终没有能够提高项目质量,说明过程是有问题或者团队没有真正接纳方法,不如放弃来节省大家的开发时间。

一说到单元测试大家肯定会先想起TDD。TDD(Test Dirven Development,测试驱动开发)是以单元测试来驱动开发的方法论。

  1. 开发一个新功能前,首先编写单元测试用例
  2. 运行单元测试,全部失败(红色)
  3. 编写业务代码,并且使对应的单元测试能够通过(绿色)
  4. 时刻维护你的单元测试,使其始终可运行

一个团队一开始就直接实施TDD的可能性是比较小的,因为适合团队的研发流程、测试底层框架封装、单元测试原则与规范都还没有敲定或者摸索出最佳的实践。直接一开始就完整实施,往往过程会变形,最终目标慢慢会偏离正轨,整个团队也不愿意再接受单元测试。所以建议是逐步开始,让团队切身能够体会到单元测试带来的收益再慢慢加码。

我们的项目基础技术架构是基于SpringCloud,做了一些基础的底层封装。项目之间的调用都是基于Feign,各个项目都是规范要提供各自的Feign接口以及Hystrix的FallbackFactory。我们将对于外部的调用都是封装在底层的service中。

单元测试范围

一个项目需要实施单元测试,首先要界定(或者说澄清)单元测试负责的范围。最常见的疑惑就是与外部系统或者其他中间件的关联,单元测试是否要实际的调用其他中间件/外部系统。

我们先来看看单元测试的定义:

Unit tests are typically automated tests written and run by software developers to ensure that a section of an application (known as the "unit") meets its design and behaves as intended.

单元测试首先应当是自动化的,由开发者编写,为了保证代码片段(最小单元)是按照预期设计实现的。我们理解就是说单元测试要保障的是项目(代码片段逻辑)自身按照设计意图正确执行,所以确认了单元测试的范围仅限于单个项目内部,因此要尽量屏蔽所有的外部系统或中间件。代码的业务逻辑覆盖80%-90%,其他部分(工具类等)不做要求。

我们项目涉及到了一些中间件(Mysql,Redis,MQ等),但是更多涉及到的内部其他支撑系统。用项目内的实际情况我们当前定义的单元测试覆盖的范围就是,单元测试从controller作为入口,尽量覆盖到controller和service所有的方法与逻辑,所有的外部接口调用全部mock,中间件尽量使用内存中间件进行mock。

单元测试基础框架

既然项目是基于SpringCloud,那测试肯定会引入基础的spring-boot-test,底层的测试框架选择是junit。

Junit主流还是junit4(Github地址)最新版本是4.12(2014年12月5日),现在最新的是junit5(JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage)。junit5正式版本的发布日期是2017年9月11日,目前最新的版本是5.5.2(2019年9月9日)。我们项目底层选择了junit5。

目前,在 Java 阵营中主要的 Mock 测试工具有 Mockito,JMock,EasyMock 等。我们选择了Mockito,这个是没有经过特别的选型。简单比较之后选择了比较容易上手并且能够满足当前需求的一款。

redis使用了redis-mock (ai.grakn:redis-mock:0.1.6)

数据库自然是使用h2(com.h2database:h2:1.4.192)(不过在一期项目我们主要服务编排,没有涉及到数据库的实例)

模拟数据生成参考了jmockdata(com.github.jsonzou:jmockdata:4.1.2),但是做了一些小小的调整增加了一些其他的类型

另外,Mockito不支持static的的方法的mock,要使用PowerMock来模拟。但是PowerMock似乎现在还不支持junit5,我们没有使用。

单元测试实施

基本框架搭建完毕,基本就进入了编码阶段。第一期的编码,我们实际上还是先写了业务代码,然后再写单元测试。接下来就详细介绍一下单元测试类的结构。这里给的示例仅仅是我们在实践过程中有使用到的,并非junit5的完整注解或者使用讲解,具体需要了解大家可以参考官网

单元测试基本结构

先看一下头部的几个注解,这些都是Junit5的

// 替换了Junit4中的RunWith和Rule
@ExtendWith(SpringExtension.class)
//提供spring依赖注入
@SpringBootTest
// 运行单元测试时显示的名称
@DisplayName("Test MerchantController")
// 单元测试时基于的配置文件
@TestPropertySource(locations = "classpath:ut-bootstrap.yml")
class MerchantControllerTest{
private static RedisServer server = null; // 下面三个mock对象是由spring提供的
@Resource
MockHttpServletRequest request; @Resource
MockHttpSession session; @Resource
MockHttpServletResponse response; // junit4中 @BeforeClass
@BeforeAll
static void initAll() throws IOException {
server = RedisServer.newRedisServer(9379);
server.start();
} // junit4中@Before
@BeforeEach
void init() {
request.addHeader("token", "test_token");
} // junit4中@After
@AfterEach
void tearDown() {
} // junit4中@AfterClass
@AfterAll
static void tearDownAll() {
server.stop();
server = null;
} }

这些都是比较基础的注解,基本也和junit4一一对应。这里没有太多可说的,可以看到我们在初始化方法中加载了虚拟的redis服务器,在前置方法中设置了Header的值

单元测试的主体方法

我们测试的主要的就是MerchantController这个类,这个类下面还有一层service方法。先看一下大概的代码印象。

    @Resource
MerchantController merchantController; @MockBean
private IOrderClient orderClient; @Test
void getStoreInfoById() {
MockConfig mockConfig = new MockConfig();
mockConfig.setEnabledCircle(true);
mockConfig.sizeRange(2, 5);
MerchantOrderQueryVO merchantOrderQueryVO = Mock.mock(MerchantOrderQueryVO.class);
StoreInfoDTO storeInfoDTO = Mock.mock(StoreInfoDTO.class,mockConfig); Mockito.when(orderClient.bizInfoV3(Mockito.any())).thenReturn(R.data(storeInfoDTO));
Mockito.when(orderClient.getOrderCount(Mockito.any())).thenReturn(R.data(merchantOrderQueryVO)); R<StoreInfoBizVO> r = merchantController.getStoreInfoById(); assertEquals(r.getData().getAvailableOrderCount(), merchantOrderQueryVO.getOrderNum());
assertEquals(r.getData().getId(), storeInfoDTO.getId());
assertEquals(r.getData().getBranchName(), storeInfoDTO.getBranchName());
} @ParameterizedTest
@ValueSource(ints = {1, 0})
void logoutCheck(Integer onlineValue) {
MockConfig mockConfig = new MockConfig();
mockConfig.setEnabledCircle(true);
mockConfig.sizeRange(2, 5);
MerchantOrderQueryVO merchantOrderQueryVO = Mock.mock(MerchantOrderQueryVO.class);
StoreInfoDTO storeInfoDTO = Mock.mock(StoreInfoDTO.class,mockConfig);
storeInfoDTO.setOnline(onlineValue);
Mockito.when(orderClient.bizInfoV3(Mockito.any())).thenReturn(R.data(storeInfoDTO));
Mockito.when(orderClient.getOrderCount(Mockito.any())).thenReturn(R.data(merchantOrderQueryVO)); R r = merchantController.logoutCheck(); if (1==onlineValue) {
assertEquals(ResourceAccessor.getResourceMessage(
MerchantbizConstant.USER_LOGOUT_CHECK_ONLINE), r.getMsg());
} else {
assertEquals(ResourceAccessor.getResourceMessage(
MerchantbizConstant.USER_LOGOUT_CHECK_UNCOMPLETED), r.getMsg());
}
} @ParameterizedTest
@CsvSource({"1,Selma,true", "2,Lisa,true", "3,Tim,false"})
void forTest(int id,String name,boolean t) {
System.out.println("id="+id+" name="+name+" tORf="+t);
merchantController.forTest(null);
}

首先看变量的部分,这里给了两个例子,一个注解是@Resource,这个是让spring来注入的。另外一个是@MockBean,这就是Mockito提供的,并且结合下面的Mockito.when方法。

接下来看方法体,我将方法主体分为三部分:

  1. Mock数据与方法

    使用Mock拦截底层的外部接口方法,并且返回随机的Mock数据(大部分数据可以使用DataMocker生成,有一些特殊有限制的,可以手动生成)。
  2. 测试方法执行

    执行目标测试方法(基本都是一行,直接调用目标方法并且返回结果)
  3. 结果断言

    根据业务逻辑预期进行断言的编写(这部分基本上没有自动化的方式,因为断言的条件和业务逻辑相关只能手动编写)

这样写下来是基本逻辑的验证,还有内部有分支逻辑,如何验证?

代码当中实际上也提到了,就是junit5提供的@ParameterizedTest注解,配合@ValueSource, @CsvSource来使用,分别可以设置指定类型或者复杂类型到单元测试中,使用方法的参数接受,定义测试不同的分支。

单元测试的执行

单元测试的执行实际上分成2部分:

  1. IDE中我们要去验证单元测试是否能够成功执行
  2. CI/CD作为执行的先决条件保障

IDE可以直接指定测试框架,我们选择junit5直接生成单元测试代码,可以直接在测试包或者类上右键执行单元测试。这个方法可以作为我们开发过程中验证待遇测试有效性的手段。但是真正要能在生产开发流程中更好的体现单元测试的价值,还是需要持续集成的支持,我们项目使用的是jenkins。依赖是Maven,以及maven-surefire-plugin插件。要特别注意一点,由于junit5还比较新,所以maven-surefire-plugin插件支持junit5还是稍微有点特殊的,参考官网说明。我们需要引入插件:

        <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M3</version>
<configuration>
<excludes>
<exclude>some test to exclude here</exclude>
</excludes>
</configuration>
</plugin>

这样在jenkins构建时就会执行单元测试,如果单元测试失败,不会触发构建后操作(Post Steps)。

总结

目前我们的项目中,单元测试的应用还在第一期,但是投入在上面的时间和精力,实际上到实际开发时间的2-3倍。因为涉及到基础框架的搭建,新框架的引入整合,底层开发编写测试代码的审核,团队的培训等等。我预计在后期,成熟的框架和流程支持下,覆盖核心业务代码的单元测试耗时应该能到实际开发工时的50%-80%左右。但是这部分的投入是能够减少测试以及线上的问题发生的概率,节省了修复的时间。

团队目前还不能完全习惯单元测试的节奏,目前带来的直接益处还不够明显,但是一个好的习惯的养成,还是需要管理者投入精力同时从上而下的推动的。

后期应该对于单元测试的执行还有一些调整或改进,而且对其概念、流程等方面应该也会有更深入和实际的理解。届时还会再次整理,并且分享给大家。

单元测试实践(SpringCloud+Junit5+Mockito+DataMocker)的更多相关文章

  1. 单元测试实践思考(junit5+jmockit+testcontainer)

    目录 背景 方案设计 单元测试指导思想 单层隔离 内部穿透 技术实现 依赖管理 基础架构 封装Junit5&Jmockit 单元测试配置 TestContainer封装 官方方案 实际方案 完 ...

  2. 我的Spark SQL单元测试实践

    最近加入一个Spark项目,作为临时的开发人员协助进行开发工作.该项目中不存在测试的概念,开发人员按需求进行编码工作后,直接向生产系统部署,再由需求的提出者在生产系统检验程序运行结果的正确性.在这种原 ...

  3. atitit.jndi的架构与原理以及资源配置and单元测试实践

    atitit.jndi的架构与原理以及资源配置and单元测试实践 1. jndi架构 1 2. jndi实现原理 3 3. jndi资源配置 3 3.1. resin  <database> ...

  4. Android单元测试实践

    为什么要写单元测试 首先要介绍为什么蘑菇街支付金融这边会采用单元测试的实践.说起来比较巧,刚开始的时候,只是我一个人会写单元测试.后来老板们知道了,觉得这是件 很有价值的事情,于是就叫我负责我们组的单 ...

  5. Android 单元测试(junit、mockito、robolectric)

    1.运用JUnit4 进行单元测试 首先在工程的 src 文件夹内创建 test 和 test/java 文件夹. 打开工程的 build.gradle(Module:app)文件,添加JUnit4依 ...

  6. 提升单元测试体验的利器--Mockito使用总结

    为神马要使用Mockito? 在编写单元测试的时候,为了尽可能的保证隔离性,我们时常需要对某些不容易构造或者不容易获取或者对外部环境有依赖的对象,用一个虚拟的对象来创建以便于测试.假设你正在开发的的代 ...

  7. MVC与单元测试实践之健身网站(一)-项目概述

    前不久刚刚通过租房网站的开发学习了MVC,并随后学习了单元测试相关的基础,现在开始健身网站的开发,该项目将结合MVC与单元测试,在开发实践过程中,趁热打铁,巩固并运用之前的内容. 一 健身网站功能描述 ...

  8. 单元测试——隔离神器:mockito

    mockito,一个让人着迷的单元测试隔离框架.对比了easymock,jmock,jmockito,最终选择了它. 为什么用他 接口语法简洁.自然.写起来像在说话,很舒服. 文档更完整.让学习曲线更 ...

  9. MVC与单元测试实践之健身网站(完)-备案与部署

    主页-http://www.zhixin9001.cn/Home/Introduce GitHub- https://github.com/zhixin9001/Fitness 这是关于Fit网站的最 ...

随机推荐

  1. Apache JMeter (一)环境的配置和操作

    JMeter是Apache组织的开放源代码项目,是一款优秀的开源测试工具,可以做功能测试和性能测试.是每个资深的测试工程师必须掌握的测试工具,熟悉JMeter可以大大提高工作效率. 1.下载Jmete ...

  2. Bootstrap4默认样式不对胃口?教你使用NPM+Webpack+SASS来定制

    Bootstrap 是一个流行的前端样式库,可以方便快速的构建应用,但默认样式可能不尽人意,本文就介绍如何使用 NPM, Webpack, SASS 针对它的源码来定制自己的主题.版本使用的是 Boo ...

  3. vmware vsphere client 创建虚拟机

    浏览器访问https://192.168.120.29 用户名:administrator@zhcs.com 密码:  Deyi123456! 说明:此案例为创建linux的Centos7的操作系统的 ...

  4. 时钟AnalogClock和DigitalClock

    <AnalogClock android:layout_width="fill_parent" android:layout_height="fill_parent ...

  5. PLC与上位机的socket通讯——ABB机器人程序(三)

    源程序:https://github.com/935094505/ABB-socket-communication 程序范例 觉得有帮助,别忘了打赏下

  6. STL目录

    觉得STL有必要讲一下,毕竟STL包含的东西太又用了. STL(Standard Template Library)这个玩意是啥,怎么来的之类的我就不说了,百度上一大推. 我就说一下ACM或者OI中会 ...

  7. 新建servlet工程

    1.选择新建Dynamic  Web Project 2.选择服务器和版本(2.5) 3.src目录下新建一个包 4.包里面新建一个类 5.实现Servlet接口(通过http协议访问) 6.serv ...

  8. 【Jenkins持续集成(一)】SonarQube 入门安装使用教程

    一.前言 持续集成管理平台不只是CI服务器,是一系列软件开发管理工具的组合. 源码版本管理:svn.git 项目构建工具:Maven.Ant 代码质量管理:Sonar(Checkstyle.PMD.F ...

  9. 记一次处理mysql数据库无故锁表的经历

    某日,生产环境上的用户表突然无故锁表,原以为只是偶发的bug.所以第一时间想到的解决方案简单粗暴:重启数据库(service mysqld restart).问题得以解决. 10min后,该表再次锁表 ...

  10. 2018年蓝桥杯java b组第八题

    标题:日志统计 小明维护着一个程序员论坛.现在他收集了一份"点赞"日志,日志共有N行.其中每一行的格式是: ts id 表示在ts时刻编号id的帖子收到一个"赞" ...