单元测试—— Mock 对象行为之 Mockito
前言
本章要介绍的工具时 Spring 的单元测试工具之一,Mockito,Mock对象的行为。
满足单一职责原则的类,都有自己独立的功能与行为,而类与类之间又是相互关联的。就像社会中的人类,人是一个独立的单元,有各自的特征和行为,但人也不可能独立而存在。这种相互关联就是相互间的依赖,要剥离开这种依赖而对单独的一个类进行测试,就必须要有工具来模拟类的依赖,而 Mockito 就是这种工具。
这里从基于 Spring MVC 的三层架构说起,大多数使用场景是将其分为 Controller层,Service层,DAO层,它们的职责独立而又相互依赖。首先看看它们各自的职责:
1,Controller层,这一层通常是定义服务的接口,对业务模块流程的控制,它由Spring MVC框架提供支持;
2,Service层,这一层通常会响应 Controller 的业务请求,并处理业务逻辑。在 Service 层的业务逻辑是比较多样的,包括业务的具体流程的设计,数据层的访问,也可能会访问中间件组件,例如缓存服务器Redis,消息服务RocketMQ,不仅如此,Service层还可能涉及到第三方服务的调用,例如调用支付宝或者微信的支付业务,百度或者高德的地图业务等等之类的接口。所以这层将作为 Unit Test 的重点。
3,DAO层,Data Access Object,顾名思义,这一层就是用于对数据库的访问与存储的。在简单的设计种,甚至可以把它纳入Service层,但是极其不推荐。单独把它抽离出来是很有道理的,首先在多数据库的设计种,它可以对外部调用屏蔽内部的实现,然后是对驱动或者依赖的升级是很友好的。这一层通常在开发时就设计并测试好了的。
确定了Spring MVC架构种各部分的职责,就把 Unit Test 的重点放 在Service 层来讲。
Service层是业务逻辑处理最复杂的一部分,它对外部有很多依赖,如果每次单元测试就将这些依赖配置一遍,那是相当困难的,甚至是不可能的,因此 Spring 提供的 Mockito 就是用来模拟这些依赖的。
Mockito 实践
首先,引入 Maven 依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.1.6.RELEASE</version>
<scope>test</scope>
</dependency>
测试的Demo类,Demo 是记录小狗动作并计分的实现,纯属虚构
package com.zhaoxj_2017.service;
import org.bson.Document;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class RewardPuppyService {
private static final String SIT_DOWN = "sitDown";
private static final String JUMP_UP = "jumpUp";
private static final String STANDING = "Standing";
/**
* 注入 DAO ,需要 Mock
*/
@Value("puppyActionCol")
private MongoCollectionService puppyActionService;
private final Map<String, Integer> rules = new HashMap<>();
private int totalScore = 0;
/**
* 创建 Bean 时需初始化的数据
*/
@Autowired
private void initRules() {
rules.put(SIT_DOWN, 5);
rules.put(JUMP_UP, 10);
rules.put(STANDING, 15);
}
public int recordPuppyActionAndScore(String puppyId, String action) {
Document record = new Document();
record.put("puppyId", puppyId);
record.put("action", action);
record.put("score", rules.get(action));
puppyActionService.insertOne(record);
totalScore += rules.get(action);
return totalScore;
}
public List<Document> getAllRecord(String puppyId) {
return puppyActionService.findMany(new Document().append("puppyId", puppyId));
}
}
测试类,通过 Mockito 模拟 @Autowired 或者 @Value 的依赖。
package com.zhaoxj_2017.service;
import org.bson.Document;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.boot.test.context.SpringBootTest;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.any;
@RunWith(MockitoJUnitRunner.class)
// @SpringBootTest 指定测试时需要的类,避免测试时加载不必要的类,也可省略。
@SpringBootTest(classes = {RewardPuppyService.class, MongoCollectionService.class})
public class RewardPuppyServiceTest {
private static final String SIT_DOWN = "sitDown";
private static final String JUMP_UP = "jumpUp";
private static final String STANDING = "Standing";
private List<Document> puppyActions = new ArrayList<>();
private String puppyId = UUID.randomUUID().toString();
public RewardPuppyServiceTest() {
Document action = new Document().append("puppyId", puppyId)
.append("action", SIT_DOWN)
.append("score", 5);
puppyActions.add(action);
}
/**
* 需要 Mock 的对象
*/
@Mock
private MongoCollectionService puppyActionService;
/**
* 被注入 Mock 对象的测试对象
*/
@InjectMocks
private RewardPuppyService rewardPuppyService;
/**
* 初始化 @Autowired 注释的方法,
* 由于 MockitoJUnitRunner 并不像 SpringJUnit4ClassRunner 能够构建 Bean,
* 因此 @Autowired 注释的方法时并不会执行的,这里通过反射完成初始化
*
* @throws NoSuchMethodException
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
@Before
public void initMocks() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method initMethod = RewardPuppyService.class.getDeclaredMethod("initRules");
initMethod.setAccessible(true);
initMethod.invoke(rewardPuppyService);
}
@Test
public void testRecordPuppyActionAndScore() {
// Mock MongoCollectionService.insertOne(Document) 的行为;
Mockito.doNothing().when(puppyActionService).insertOne(any(Document.class));
Assert.assertEquals(5, rewardPuppyService.recordPuppyActionAndScore(puppyId, SIT_DOWN));
Assert.assertEquals(15, rewardPuppyService.recordPuppyActionAndScore(puppyId, JUMP_UP));
Assert.assertEquals(30, rewardPuppyService.recordPuppyActionAndScore(puppyId, STANDING));
}
@Test
public void testGetAllRecord() {
// Mock MongoCollectionService.findMany(Document) 的行为;
Mockito.when(puppyActionService.findMany(any())).thenReturn(puppyActions);
Assert.assertEquals(puppyActions, rewardPuppyService.getAllRecord(puppyId));
}
}
总结
Mockito 的功能集非常的多,能够轻松的模拟套件。上述实践能满足大多数的 Unit Test,直接把指定的 Service 类放入模拟的环境中,完全隔离它的依赖而运行,其功能是非常之多的,更多功能大家可以参考《Mockito Javadoc》一起来探究