一、序言
针对功能做测试的时候,我们经常会有单元测试和集成测试,在实际开发过程中发现有很多童鞋经常混淆这两个内容,在分享Mockito使用过程前先区分这两个概念。
二、测试分类和区别
所谓单元测试,其实就是对单个方法内部逻辑的测试,不涉及关联其他分层的代码逻辑测试,比如定义服务层UserService和Dao层UserDao,两个类分别存在save方法,并且前者在方法内部调用后者方法,那么对UserService的save方法进行单元测试只是针对内部逻辑测试,要屏蔽掉UserDao相关方法调用返回值影响。单元测试的特点就是执行速度快,单一的结构,不依赖其他容器。
而集成测试恰好与单元测试相反,它需要将关联类的行为纳入测试结果,便于测试系统各个组件集成后是否能运行正确,更多针对的是整个处理流程。
三、什么是Mock测试
Mock 测试是单元测试的一种形式,通过在测试过程中创建一个假的对象,避免为了测试一个方法,需要自行构建整个 Bean 的依赖链。比如下图,类 A 需要调用类 B 和类 C,而类 B 和类 C 又需要调用其他类如 D、E、F 等,假设类 D 是一个外部服务,那就会很难测试,因为返回结果会直接受下游服务的影响,导致单元测试流程受阻。
而 Mock 测试,就是帮忙解决这个问题。它可以创建一个假的对象,替换掉真实的 Bean B 和 C,这样在方法调用时,实际上就会去调用 Mock 对象的方法,而 Mock 对象又可以设置我们自己的参数值和期望的返回值,让我们可以专注在自己的测试范围内,而不会受到其他的下游服务影响,从而提高整个单元测试的效率。引入 Mock 测试之后整个流程变化如下:
四、什么是 Mockito
Mockito 是一种 Java Mock 框架,他主要就是用来做 Mock 测试的,它可以模拟任何 Spring 管理的 Bean、方法返回值、异常抛出等等,同时也会记录调用这些模拟方法的参数、调用顺序,从而可以校验出这个 Mock 对象是否有被正确的顺序调用,以及按照期望的参数被调用。
比如 Mockito 可以在单元测试中模拟一个 Service 返回的数据,而不会真正去调用该 Service,这就是上面提到的 Mock 测试精神,也就是通过模拟一个假的 Service 对象,来快速的测试当前想要测试的类。目前在 Java 中主流的 Mock 测试工具有 Mockito、JMock、EasyMock等等,而 SpringBoot 目前内建的是 Mockito 框架。
五、单元测试编写
引入依赖,改依赖包含 JUnit 和 Mockito。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
创建 UserService 和 UserDao,UserService服务提供两个方法 getUserById() 和 insertUser(),分别调用 UserDao 的 getUserById() 和 insertUser() 两个方法,具体代码如下:
@Component
public class UserService {
@Autowired
private UserDao userDao;
public User getUserById(Integer id) {
return userDao.getUserById(id);
}
public Integer insertUser(User user) {
return userDao.insertUser(user);
}
}
定义 User 实体:
public class User {
private Integer id;
private String name;
/**
* 此处省略get/set方法
**/
}
假设当前不使用 Mockito,测试代码大概如下:
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void getUserById() throws Exception {
//查询数据库获取数据
User user = userService.getUserById(1);
Assert.assertNotNull(user);
}
}
整体上注入 userService 服务,编写对应的测试方法,测试方法内部再去调用 userDao 查询数据库的数据,再对返回结果做 Assert 断言检查(这种方式严格来说是属于集成测试,因为涉及到分层)。但是此时 userDao 还没写好,又想单独测试 userService 方法,那么就可以使用 Mockito 模拟假的 userDao 出来,定义其行为方式,这种方式称为stub(存根或者打桩)。
简单理解就是把所需要的测试数据塞到一个对象里,重点关注测试目标的方法,对于不易构造或者不易获取对象和方法都采用桩来代替。
Mockito 提供 @MockBean 注解标注模拟的对象,当加上这个注解之后,Mockito 会帮我们创建一个假的 Mock 对象替换调原有真实的对象,这时候再去注入的 Dao 就已经被替换成 Mock 对象,同时还可以定义具体的参数和返回值,具体用法如下:
Mockito.when(对象.方法名()).thenReturn(自定义结果);
使用 Mockito 之后,上面单元测试的代码就可以变成如下:
@RunWith(SpringRunner.class)
@SpringBootTest
publicclass UserServiceTest {
@Autowired
private UserService userService;
@MockBean
private UserDao userDao;
@Test
public void getUserById() throws Exception {
// 定义当调用mock userDao的getUserById()方法,并且参数为3时,就返回id为200、name为I'm mock3的user对象
Mockito.when(userDao.getUserById(3)).thenReturn(new User(200, "I'm mock 3"));
// 返回的会是名字为I'm mock 3的user对象
User user = userService.getUserById(3);
Assert.assertNotNull(user);
}
}
上面 @mockBean 也可以改成使用 @Mock 注解。
两者的区别主要在于 @Mock 是 Mockito 提供的包;如果不依赖 Spring 容器的测试,那么使用 @mock 就足够了。而 @MockBean 是 spring 提供的注解,需要依赖 Spring 容器,如果需要 mock 被 Spring 管理的 bean,那么就用 @MockBean。
Mockito 除了上面最基本的条件返回自定义结果方法,提供了其他用法。
六、扩展方法
6.1 ThenReturn 系列方法
限制只有指定参数时,才会回传相应数据。例如:当参数的数字是 3 时,才会回传名字为 I’m mock 3 的 对象。
Mockito.when(userService.getUserById(3)).thenReturn(new User(3, "I'm mock"));
// 回传的user的名字为I'm mock
User user1 = userService.getUserById(3);
// 回传的user为null
User user2 = userService.getUserById(200);
可以指定任何参数,都可以返回数据,比如:使用任何整数值调用 userService 的 getUserById() 方法时,就回传一个名字为 I’m mock3 的 User 对象。
Mockito.when(userService.getUserById(Mockito.anyInt())).thenReturn(new User(3, "I'm mock"));
// 回传的user的名字为I'm mock
User user1 = userService.getUserById(3);
// 回传的user的名字也为I'm mock
User user2 = userService.getUserById(200);
不管传入对象参数是什么,都返回指定的值,比如:任何当调用 userService 的 insertUser() 方法时,都回传 100。
Mockito.when(userService.insertUser(Mockito.any(User.class))).thenReturn(100);
//会返回100
Integer i = userService.insertUser(new User());
6.2 ThenThrow 系列方法
指定调用方法时,跑出异常,例如:当调用 userService 的 getUserById() 时的参数是 9 时,抛出 RuntimeException。
Mockito.when(userService.getUserById(9)).thenThrow(new RuntimeException("mock throw exception"));
// 抛出RuntimeException
User user = userService.getUserById(9);
如果方法本身没有返回值,可以使用 doThrow() 抛出 Exception。
Mockito.doThrow(new RuntimeException("mock throw exception")).when(userService).print();
//抛出一个RuntimeException
userService.print();
6.3 Verify 系列方法
用来检验某个方法的指定参数是否被调用过,这个主要可以用来测试一些异常边界以及特殊场景。比如:检查调用 userService 的 getUserById()、且参数为3的次数是否为1次。
Mockito.verify(userService, Mockito.times(1)).getUserById(Mockito.eq(3)) ;
验证调用顺序,校验 userService 是否先调用 getUserById() 两次,并且第一次的参数是 3、第二次的参数是 5,然后才调用 insertUser() 方法。
InOrder inOrder = Mockito.inOrder(userService);
inOrder.verify(userService).getUserById(3);
inOrder.verify(userService).getUserById(5);
inOrder.verify(userService).insertUser(Mockito.any(User.class));
七、总结
单元测试框架 Mockito 很方便的为我们构建模拟环境,方便我们对代码进行测试,但我觉得更多需要的则是开发人员思维的转变,在原先集成测试的基础上,更细粒度的处理单元测试,再配合相应CodeReview和流程上的规范约束,这样才能较大限度的保证代码质量。