mock测试及jacoco覆盖率
单元测试是保证项⽬代码质量的有⼒武器,但是有些业务场景,依赖的第三⽅没有测试环境,这时候该怎么做Unit Test呢,总不能直接⽣产环境硬来吧?
可以借助⼀些mock测试⼯具来解决这个难题(⽐如下⾯要讲的mockito),废话不多说,直奔主题:
⼀、准备⽰例Demo
假设有⼀个订单系统,⽤户可以创建订单,同时下单后要检测⽤户余额(如果余额不⾜,提醒⽤户充值),具体来说,⾥⾯有2个服务:OrderService、UserService,类图如下:
⽰例代码:
package comblogs.yjmyzz.springbootdemo.service.impl;
import comblogs.yjmyzz.springbootdemo.service.UserService;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
* @author 菩提树下的杨过
*/
@Service("userService")
public class UserServiceImpl implements UserService {
@Override
public BigDecimal queryBalance(int userId) {
System.out.println("queryBalance=>userId:" + userId);
//模拟返回100元余额
return new BigDecimal(100);
}
}
及
package comblogs.yjmyzz.springbootdemo.service.impl;
import comblogs.yjmyzz.springbootdemo.service.OrderService;
import comblogs.yjmyzz.springbootdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
@Service("orderService")
public class OrderServiceImpl implements OrderService {
@Autowired
private UserService userService;
/**
* 下订单
*
* @param productName
* @param orderNum
* @return
* @throws Exception
*/
@Override
public Long createOrder(String productName, Integer orderNum, int userId) throws Exception {
System.out.println("createOrder=>userId:" + userId);
if (StringUtils.isEmpty(productName)) {
throw new Exception("productName is empty");
}
if (orderNum == null) {
throw new Exception("orderNum is null!");
}
if (orderNum <= 0) {
throw new Exception("orderNum must bigger than 0");
}
/
/下订单过程略,返回1L做为订单号
Long orderId = 1L;
//模拟检测余额
BigDecimal balance = userService.queryBalance(userId);
if (balancepareTo(BigDecimal.TEN) <= 0) {
System.out.println("余额不⾜10元,请及时充值!");
}
return orderId;
}
}
⾥⾯的逻辑不是重点,随便看看就好。关注下createOrder⽅法,最后⼏⾏OrderService调⽤了UserServ
ice查询余额,即:OrderService依赖UserService,假设UserService就是⼀个第3⽅服务,不具备测试环境,本⽂就来讲讲如何对UserService进⾏mock测试。
⼆、pom引⼊mockito 及 jacoco plugin
2.1引⼊mockito
1<dependency>
2<groupId&kito</groupId>
3<artifactId>mockito-all</artifactId>
4<version>1.9.5</version>
5<scope>test</scope>
6</dependency>
View Code
mockito是⼀个mock⼯具库,马上会讲到⽤法。
2.2引⼊jacoco插件
1<plugin>
2<groupId>org.jacoco</groupId>
3<artifactId>jacoco-maven-plugin</artifactId>
4<version>0.8.5</version>
5<executions>
6<execution>
7<id>prepare-agent</id>
8<goals>
9<goal>prepare-agent</goal>
10</goals>
11</execution>
12<execution>
13<id>report</id>
14<phase>prepare-package</phase>
15<goals>
16<goal>report</goal>
17</goals>
18</execution>
19<execution>
20<id>post-unit-test</id>
21<phase>test</phase>
22<goals>
23<goal>report</goal>
24</goals>
25<configuration>
26<dataFile></dataFile>
27<outputDirectory>target/jacoco-ut</outputDirectory>
28</configuration>
29</execution>
30</executions>
31</plugin>
View Code
可以将单元测试的结果,直接⽣成html⽹页,分析代码覆盖率。注意 <outputDirectory>target/jacoco-ut</outputDirectory> 这⼀⾏的配置,表⽰将在target/jacoco-ut⽬录下⽣成测试报告。
三、编写单测⽤例
3.1约定⼤于规范
以OrderServiceImpl类为例,如果要对它做单元测试,建议按以下约定:
a.在test/java下创建⼀个与OrderServiceImpl同名的package名(注:这样的好处是测试类与原类,处于同1个包,代码可见性相同)
b.然后在该package下创建OrderServiceImplTest类(注意:⼀般测试类名的风格为 xxxxTest,在原类名后加Test)
3.2单元测试模板
参考下⾯的代码模板:
package comblogs.yjmyzz.springbootdemo.service.impl;
import comblogs.yjmyzz.springbootdemo.service.UserService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
kito.InjectMocks;
kito.Mock;
kito.MockitoAnnotations;
kito.runners.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class OrderServiceImplTest {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
/**
* 真正要测试的类
*/
@InjectMocks
private OrderServiceImpl orderService;
/**
* 测试类依赖的其它服务
*/
@Mock
private UserService userService;
/**
* createOrder成功时的⽤例
*/
@Test
public void testCreateOrderSuccess() {
//todo
}
/**
* createOrder失败时的⽤例
*/
@Test
public void testCreateOrderFailure() {
//todo
}
}
讲解⼀下:
a.类上的@RunWith要改成 MockitoJUnitRunner.class,否则mockito不⽣效
b.真正需要测试的类,要⽤@InjectMocks,⽽不是@Mock(更不能是@Autowired)
-- 原因1:@Autowired是Spring的注解,在mock环境下,根本就没有Spring上下⽂,当然会注⼊失败。
-- 原因2:也不能是@Mock,@Mock表⽰该注⼊的对象是“虚构”的假对象,⾥⾯的⽅法代码根本不会真
正运⾏,统⼀返回空对象null,即:被@Mock修饰的对象,在该测试类中,其具体的代码永远⽆法覆盖到!这也就是失败了单元测试的意义。⽽@InjectMocks修饰的对象,被测试的⽅法,才会真正进⼊执⾏。
另外,测试服务时,被mock注⼊的类,应该是具体的服务实现类,即:xxxService Impl,⽽不是服务接⼝,在mock环境中接⼝是⽆法实例化的。
c.通常⼀个⽅法,会有运⾏成功和运⾏失败⼆种情况,建议测试类⾥,⽤test XXX Success以及test XXX Failure区分开来,看起来⽐较清晰。
3.3测试覆盖率
先来看看下单失败的情况:下单前有很多参数校验,先验证下这些参数异常的场景。
public int userId = 101;
/**
* createOrder失败时的⽤例
*/
@Test
public void testCreateOrderWhenFail() {
try {
} catch (Exception e) {
Assert.assertEquals(true, true);
}
try {
} catch (Exception e) {
Assert.assertEquals(true, true);
}
try {
} catch (Exception e) {
Assert.assertEquals(true, true);
}
try {
} catch (Exception e) {
Assert.assertEquals(true, true);
}
}
命令⾏下mvn package 跑⼀下单元测试,全通过后,会在target/jacoco-ut ⽬录下⽣成⽹页报告
浏览器打开index.html,就能看到覆盖率
可以看到,中间那个带部分绿⾊的,就是我们刚才写过单测的pacakge,⼀层层点下去,能看到ateOrder⽅法的代码覆盖情况,绿⾊的⾏表⽰覆盖到了,红⾊的表⽰未覆盖。
讲⼀个⼩技巧:有些类,⽐如DAO/Mytatis层⾃动⽣成的DO/Entity,还有⼀些常量定义等,其实没什么测试的必要,可以排除掉,这样不仅可以提⾼测试的覆盖率,还能让我们更关注于核⼼业务类的测试。
排除的⽅法很简单,可jacoco插件⾥配置exclude规则即可,参考下⾯这样:
<configuration>
<dataFile></dataFile>
<outputDirectory>target/jacoco-ut</outputDirectory>
<excludes>
springframework包<exclude>
**/cnblogs/yjmyzz/**/aspect/**,
**/yjmyzz/**/SampleApplication.class
</exclude>
</excludes>
</configuration>
View Code
这样就把aspect包下的所有类,以及SampleApplication.class这个特定类给排除在单元测试之外,此时再跑⼀下mvn package ,对⽐下重新⽣成的报告
覆盖率从刚才的26%上升到了61%
3.4 mock返回值
从覆盖率上看,刚才createOrder⽅法⾥,最后⼏⾏并没有覆盖到,可以再写⼀个⽤例
问题来了,报异常了!分析下UserService的queryBalance⽅法实现
@Override
public BigDecimal queryBalance(int userId) {
System.out.println("queryBalance=>userId:" + userId);
//模拟返回100元余额
return new BigDecimal(100);
}
已经写死了返回100元,不应该为Null对象,同时还输出了⼀⾏⽇志,但是从测试结果来看,这个⽅法并没有真正执⾏。这也就印证了
@Mock修饰的对象,是“假”的,并不会真正执⾏内部的代码
@Test
public void testCreateOrderSuccess() throws Exception {
BigDecimal balance = BigDecimal.TEN;
//表⽰:当userService.queryBalance(userId)执⾏时,将返回balance变量做为返回值
when(userService.queryBalance(userId)).thenReturn(balance);
long orderId = ateOrder("phone", 10, userId);
Assert.assertEquals(orderId, 1L);
}
把测试代码调整下,改成上⾯这样,利⽤when(...).thenReturn(...),表⽰当xxx⽅法执⾏时,将模拟返回yyy对象。这样就mock出了userService的返回值
现在测试就通过了,再看看⽣成的测试报告,最后⼏⾏,也被覆盖到了。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论