单元测试是保证项目代码质量的有力武器,但是有些业务场景,依赖的第三方没有测试环境,这时候该怎么做Unit Test呢,总不能直接生产环境硬来吧?
可以借助一些mock测试工具来解决这个难题(比如下面要讲的mockito),废话不多说,直奔主题:
一、准备示例Demo
假设有一个订单系统,用户可以创建订单,同时下单后要检测用户余额(如果余额不足,提醒用户充值),具体来说,里面有2个服务:OrderService、UserService,类图如下:
示例代码:
package com.cnblogs.yjmyzz.springbootdemo.service.impl;
import com.cnblogs.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 com.cnblogs.yjmyzz.springbootdemo.service.impl;
import com.cnblogs.yjmyzz.springbootdemo.service.OrderService;
import com.cnblogs.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 (balance.compareTo(BigDecimal.TEN) <= 0) {
System.out.println("余额不足10元,请及时充值!");
}
return orderId;
}
}
里面的逻辑不是重点,随便看看就好。关注下createOrder方法,最后几行OrderService调用了UserService查询余额,即:OrderService依赖UserService,假设UserService就是一个第3方服务,不具备测试环境,本文就来讲讲如何对UserService进行mock测试。
二、pom引入mockito 及 jacoco plugin
2.1 引入mockito
1 <dependency>
2 <groupId>org.mockito</groupId>
3 <artifactId>mockito-all</artifactId>
4 <version>1.9.5</version>
5 <scope>test</scope>
6 </dependency>
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>target/jacoco.exec</dataFile>
27 <outputDirectory>target/jacoco-ut</outputDirectory>
28 </configuration>
29 </execution>
30 </executions>
31 </plugin>
jacoco可以将单元测试的结果,直接生成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 com.cnblogs.yjmyzz.springbootdemo.service.impl;
import com.cnblogs.yjmyzz.springbootdemo.service.UserService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.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注入的类,应该是具体的服务实现类,即:xxxServiceImpl,而不是服务接口,在mock环境中接口是无法实例化的。
c. 通常一个方法,会有运行成功和运行失败二种情况,建议测试类里,用testXXXSuccess以及testXXXFailure区分开来,看起来比较清晰。
3.3 测试覆盖率
先来看看下单失败的情况:下单前有很多参数校验,先验证下这些参数异常的场景。
public int userId = 101;
/**
* createOrder失败时的用例
*/
@Test
public void testCreateOrderWhenFail() {
try {
orderService.createOrder(null, 10, userId);
} catch (Exception e) {
Assert.assertEquals(true, true);
}
try {
orderService.createOrder("book", null, userId);
} catch (Exception e) {
Assert.assertEquals(true, true);
}
try {
orderService.createOrder("book", 0, userId);
} catch (Exception e) {
Assert.assertEquals(true, true);
}
try {
orderService.createOrder("book", 50, userId);
} catch (Exception e) {
Assert.assertEquals(true, true);
}
}
命令行下mvn package 跑一下单元测试,全通过后,会在target/jacoco-ut 目录下生成网页报告
浏览器打开index.html,就能看到覆盖率
可以看到,中间那个带部分绿色的,就是我们刚才写过单测的pacakge,一层层点下去,能看到OrderServiceImpl.createOrder方法的代码覆盖情况,绿色的行表示覆盖到了,红色的表示未覆盖。
讲一个小技巧:有些类,比如DAO/Mytatis层自动生成的DO/Entity,还有一些常量定义等,其实没什么测试的必要,可以排除掉,这样不仅可以提高测试的覆盖率,还能让我们更关注于核心业务类的测试。
排除的方法很简单,可jacoco插件里配置exclude规则即可,参考下面这样:
<configuration>
<dataFile>target/jacoco.exec</dataFile>
<outputDirectory>target/jacoco-ut</outputDirectory>
<excludes>
<exclude>
**/cnblogs/yjmyzz/**/aspect/**,
**/yjmyzz/**/SampleApplication.class
</exclude>
</excludes>
</configuration>
这样就把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 = orderService.createOrder("phone", 10, userId);
Assert.assertEquals(orderId, 1L);
}
把测试代码调整下,改成上面这样,利用when(...).thenReturn(...),表示当xxx方法执行时,将模拟返回yyy对象。这样就mock出了userService的返回值
现在测试就通过了,再看看生成的测试报告,最后几行,也被覆盖到了。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有