SpringBoot 单元测试利器——Mockito

Mockito 是一种 Java mock 框架,他主要是用来做 mock 测试的,他可以模拟任何 Spring 管理的 bean、模拟方法的返回值、模拟抛出异常…等,在了解 Mockito 的具体用法之前,得先了解什麽是 mock 测试

1. 什么是 mock 测试?

mock 测试就是在测试过程中,创建一个假的对象,避免你为了测试一个方法,却要自行构建整个 bean 的依赖链

像是以下这张图,类 A 需要调用类 B 和类 C,而类 B 和类 C 又需要调用其他类如 D、E、F 等,假设类 D 是一个外部服务,那就会很难测,因为你的返回结果会直接的受外部服务影响,导致你的单元测试可能今天会过、但明天就过不了了

SpringBoot 单元测试利器——Mockito

而当我们引入 mock 测试时,就可以创建一个假的对象,替换掉真实的 bean B 和 C,这样在调用B、C的方法时,实际上就会去调用这个假的 mock 对象的方法,而我们就可以自己设定这个 mock 对象的参数和期望结果,让我们可以专注在测试当前的类 A,而不会受到其他的外部服务影响,这样测试效率就能提高很多

SpringBoot 单元测试利器——Mockito

2. Mockito 简介

说完了 mock 测试的概念,接下来我们进入到今天的主题,Mockito

Mockito 是一种 Java mock 框架,他主要就是用来做 mock 测试的,他可以模拟任何 Spring 管理的 bean、模拟方法的返回值、模拟抛出异常…等,他同时也会记录调用这些模拟方法的参数、调用顺序,从而可以校验出这个 mock 对象是否有被正确的顺序调用,以及按照期望的参数被调用

像是 Mockito 可以在单元测试中模拟一个 service 返回的数据,而不会真正去调用该 service,这就是上面提到的 mock 测试精神,也就是通过模拟一个假的 service 对象,来快速的测试当前我想要测试的类

目前在 Java 中主流的 mock 测试工具有 Mockito、JMock、EasyMock..等,而 SpringBoot 目前内建的是 Mockito 框架

题外话说一下,Mockito 是命名自一种调酒莫吉托(Mojito),外国人也爱玩谐音梗。。。

3. 在 SpringBoot 单元测试中使用 Mockito

首先在 pom.xml 下新增 spring-boot-starter-test 依赖,该依赖内就有包含了 JUnit、Mockito

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

先写好一个 UserService,他里面有两个方法 getUserById()insertUser(),而他们会分别去再去调用 UserDao 这个 bean的 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 model 的定义如下

public class User {
private Integer id;
private String name;
//省略 getter/setter
}

如果这时候我们先不使用 Mockito 模拟一个假的 userDao bean,而是真的去调用一个正常的 Spring bean 的 userDao 的话,测试类写法如下。其实就是很普通的注入 userService bean,然后去调用他的方法,而他会再去调用 userDao 取得数据库的数据,然后我们再对返回结果做 assert 断言检查

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

//先普通的注入一个userService bean
@Autowired
private UserService userService;

@Test
public void getUserById() throws Exception {
//普通的使用userService,他里面会再去调用userDao取得数据库的数据
        User user = userService.getUserById(1);

//检查结果
        Assert.assertNotNull(user);
        Assert.assertEquals(user.getId(), new Integer(1));
        Assert.assertEquals(user.getName(), "John");
    }
}

但是如果 userDao 还没写好,又想先测 userService 的话,就需要使用 Mockito 去模拟一个假的 userDao 出来

使用方法是在 userDao 上加上一个 @MockBean 注解,当 userDao 被加上这个注解之后,表示 Mockito 会帮我们创建一个假的 mock 对象,替换掉 Spring 中已存在的那个真实的 userDao bean,也就是说,注入进 userService 的 userDao bean,已经被我们替换成假的 mock 对象了,所以当我们再次调用 userService 的方法时,会去调用的实际上是 mock userDao bean 的方法,而不是真实的 userDao bean

当我们创建了一个假的 userDao 后,我们需要为这个 mock userDao 自定义方法的返回值,这里有一个公式用法,下面这段代码的意思为,当调用了某个 mock 对象的方法时,就回传我们想要的自定义结果

Mockito.when( 对象.方法名() ).thenReturn( 自定义结果 )

使用 Mockito 模拟 bean 的单元测试具体实例如下

@RunWith(SpringRunner.class)
@SpringBootTest
public class 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(1);

        Assert.assertNotNull(user);
        Assert.assertEquals(user.getId(), new Integer(200));
        Assert.assertEquals(user.getName(), "I'm mock 3");
    }
}

Mockito 除了最基本的 Mockito.when( 对象.方法名() ).thenReturn( 自定义结果 ),还提供了其他用法让我们使用

thenReturn 系列方法

当使用任何整数值调用 userService 的 getUserById() 方法时,就回传一个名字为 I’m mock3 的 user 对象

Mockito.when(userService.getUserById(Mockito.anyInt())).thenReturn(new User(3, "I'm mock"));
User user1 = userService.getUserById(3); // 回传的user的名字为I'm mock
User user2 = userService.getUserById(200); // 回传的user的名字也为I'm mock

限制只有当参数的数字是 3 时,才会回传名字为 I’m mock 3 的 user 对象

Mockito.when(userService.getUserById(3)).thenReturn(new User(3, "I'm mock"));
User user1 = userService.getUserById(3); // 回传的user的名字为I'm mock
User user2 = userService.getUserById(200); // 回传的user为null

当调用 userService 的 insertUser() 方法时,不管传进来的 user 是什麽,都回传 100

Mockito.when(userService.insertUser(Mockito.any(User.class))).thenReturn(100);
Integer i = userService.insertUser(new User()); //会返回100

thenThrow 系列方法

当调用 userService 的 getUserById() 时的参数是 9 时,抛出一个 RuntimeException

Mockito.when(userService.getUserById(9)).thenThrow(new RuntimeException("mock throw exception"));
User user = userService.getUserById(9); //会抛出一个RuntimeException

如果方法没有返回值的话(即是方法定义为public void myMethod() {...}),要改用 doThrow() 抛出 Exception

Mockito.doThrow(new RuntimeException("mock throw exception")).when(userService).print();
userService.print(); //会抛出一个RuntimeException

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));

4. Mockito 的限制

上述就是 Mockito 的 mock 对象使用方法,不过当使用 Mockito 在 mock 对象时,有一些限制需要遵守

  • 不能 mock 静态方法
  • 不能 mock private 方法
  • 不能 mock final class

因此在写代码时,需要做良好的功能拆分,才能够使用 Mockito 的 mock 技术,帮助我们降低测试时 bean 的耦合度

5. 总结

Mockito 是一个非常强大的框架,可以在执行单元测试时帮助我们模拟一个 bean,提高单元测试的稳定性

并且大家可以尝试在写代码时,从 mock 测试的角度来写,更能够写出功能切分良好的代码架构,像是如果有把专门和外部服务沟通的代码抽出来成一个 bean,在进行单元测试时,只要透过 Mockito 更换掉那个 bean 就行了

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

关注我们