单元测试实践(SpringCloud+Junit5+Mockito+DataMocker)
⽹上看过⼀句话,单元测试就像早睡早起,每个⼈都说好,但是很少有⼈做到。从这么多年的项⽬经历亲⾝证明,是真的。
这次借着项⽬内实施单元测试的机会,记录实施的过程和⼀些总结经验。
项⽬情况
⾸先是背景,项⽬是⼀个较⼤型的项⽬,多个团队协作开发,采⽤的是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()最新版本是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")
bootstrap 5// 单元测试时基于的配置⽂件
@TestPropertySource(locations = "l")
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 = wRedisServer(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 = k(MerchantOrderQueryVO.class);
StoreInfoDTO storeInfoDTO = k(StoreInfoDTO.class,mockConfig);
Mockito.when(orderClient.bizInfoV3(Mockito.any())).thenReturn(R.data(storeInfoDTO));
Mockito.OrderCount(Mockito.any())).thenReturn(R.data(merchantOrderQueryVO));
R<StoreInfoBizVO> r = StoreInfoById();
Data().getAvailableOrderCount(), OrderNum());
Data().getId(), Id());
Data().getBranchName(), BranchName());
}
@ParameterizedTest
@ValueSource(ints = {1, 0})
void logoutCheck(Integer onlineValue) {
MockConfig mockConfig = new MockConfig();
mockConfig.setEnabledCircle(true);
mockConfig.sizeRange(2, 5);
MerchantOrderQueryVO merchantOrderQueryVO = k(MerchantOrderQueryVO.class);
StoreInfoDTO storeInfoDTO = k(StoreInfoDTO.class,mockConfig);
storeInfoDTO.setOnline(onlineValue);
Mockito.when(orderClient.bizInfoV3(Mockito.any())).thenReturn(R.data(storeInfoDTO));
Mockito.OrderCount(Mockito.any())).thenReturn(R.data(merchantOrderQueryVO));
R r = merchantController.logoutCheck();
if (1==onlineValue) {
ResourceMessage(
MerchantbizConstant.USER_LOGOUT_CHECK_ONLINE), r.getMsg());
} else {
ResourceMessage(
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%左右。但是这部分的投⼊是能够减少测试以及线上的问题发⽣的概率,节省了修复的时间。
团队⽬前还不能完全习惯单元测试的节奏,⽬前带来的直接益处还不够明显,但是⼀个好的习惯的养成,还是需要管理者投⼊精⼒同时从上⽽下的推动的。
后期应该对于单元测试的执⾏还有⼀些调整或改进,⽽且对其概念、流程等⽅⾯应该也会有更深⼊和实际的理解。届时还会再次整理,并且分享给⼤家。