Bootstrap

基于 Mockito 框架的 Mock 测试

1.什么是 Mock 测试

Mock 通常是指,在测试一个对象 A 时,我们构造一些假的对象(一般是不容易构造或者不容易获取的对象,比如测试类所依赖的实现类、第三方接口、数据库操作对象)来模拟与 A 之间的交互,这些对象被称为 Mock 对象,而 Mock 对象的行为是我们事先设定且符合预期。通过这些 Mock 对象来测试 A 在正常逻辑,异常逻辑或压力情况下工作是否正常。

下面是一个例子:

image.png|400

当我们需要测试 OrderService 时,按照我们常规的做法呢,都是要先准备好 redis,跟 db 的环境,然后构造UserService 跟 CouponService 注入进来,此时需要构建完整的依赖树,其过程是比较繁琐的,万一数据库连不上,依赖找不到,服务挂了… 时间一长可能会打击我们对项目进行单测的积极性,所以这时候很有必要寻求一种优雅的方式来解决。即,把这些依赖都转成 Mock 对象,作为模拟,将测试的重点专注于开发的功能逻辑。

2.常用注解

2.1@Mock

创建一个模拟对象,并注入到测试类中。通常和 @InjectMocks 注解一起使用,用于为测试类中的被测对象注入模拟对象。当你使用 @Mock 注解一个对象时,Mockito 会为你创建一个该对象的模拟实例。这个模拟实例的行为(即方法的返回值和它们被调用的方式)可以被精确地控制和验证。默认情况下,模拟对象上的所有方法调用都会返回 null(对于引用类型)或默认值(对于原始类型)。

可以通过插桩的方式来模拟对象的行为。

2.2@Spy

用于创建一个部分模拟对象,它也属于 mock 对象的一种。但与@Mock不同,@Spy创建的是一个实际对象的代理,并且允许你选择性地模拟对象中的某些方法。默认情况下,所有未被特别模拟的方法都会调用实际对象的方法,通常与 doReturn/doThrow 等方法结合使用。一般是加在要测试的方法所在的类对象上,即加了 @InjectMocks 注解的位置。

2.3@InjectMocks

一般标注在被测对象上(必须是实现类),因为 mockito 会为添加了 @InjectMocks 注解的属性创建对应的实例对象。默认创建的对象就是一个普通的对象。经常需要配合 @Spy 注解使其变为默认调用真实方法的 mock 对象(可以理解为一个支持插桩的真实对象)。此外,mockito 会将 @Mock 和 @Spy 标注的对象注入到添加了 @InjectMocks 注解的被测对象中。

@InjectMocks
@Spy
private UserServiceImpl UserServiceImplTest;

@Mock
private UserFeatureService userFeatureService;

@Mock
private List<String> mockList;

一点总结:

方法插桩方法不插桩作用对象
Mock 对象执行插桩逻辑返回 mock 对象的默认值(不会去调用真实实现类方法)被测试类的依赖项
Spy 对象执行插桩逻辑调用真实方法被测试的实现类

3.常见操作

3.1插桩

  • when(xxxService.someMethod()).thenXxx(…):其中 xxxService 可以是 mock 对象。
  • doXXX().when(xxxService).someMethod():其中 xxxService 可以是 mock/spy 对象。

推荐是使用 doXXX()… 的插桩方式,因为对于 spy 对象,如果采用 when(…)… 的方式,会先执行真实的方法,比如会先执行一段打印的逻辑,而这不是我们想要的(即使由于插桩,最终返回的是插桩时指定的返回)。

常见的几种用法:

  • 指定返回
  • 指定抛出异常
  • 多次插桩
  • thenAnswer 指定实现自定义逻辑的插桩
@Test
public void test()(
    when(mockList.get(anyInt())).thenAnswer(newAnswer<String>(){
        /**
        *泛型表示要插桩方法的返回值类型
        */
        @override
        public String answer(InvocationOnMock invocation) throws Throwable{
            // getArgument表示获取插桩方法(此处就是List.get)的第几个参数值
            Integer argument = invocation.getArgument(index: 0, Integer.class);
            return String.valueof(argument* 100);
        }
});
    String result = mockList.get(3);
    Assertions.assertEquals(expected:"3ee",result);
}

3.2执行真实的方法

Mock 对象也可以执行真实的方法,只要在插桩时指定让他去调用真实方法即可。

Spy 对象,不插桩时即是执行真实方法。

3.3verify

可以用于验证某个方法是否被调用、被调用了几次、至少/至多调用了几次。且可以指定匹配的参数。此外,即使该方法被调用多次,但参数未匹配上,那也是不算的。

Verify 的另一个作用是对没有返回值的方法进行验证,证明其至少是被调用过的。

@Mock
private UserService userService;

@Test
public void test1() {
    userService.addUser("张三", "25", "男")
    // 通过
    verify(userService, times(1)).addUser("张三","25","男");
    // 不通过
    verify(userService, times(1)).addUser("李四","25","男");
    // 通过
    verify(userService, times(1)).addUser(anyString(),anyString(),anyString());
}

3.4断言

断言可用于对有返回值的方法进行判断。

也可以对没有返回值的方法进行判断:

Assertions.assertDoesNotThrow(() -> userServiceImplUnderTest.add("张三","25"));

4.基本思路

  1. 对于非测试方法进行 mock(一般是插桩),一般是数据库,或其他类方法的调用;
  2. 调用需要测试的方法;
  3. 实时修改(插桩、添加断言等);

5.参考

  1. 视频,有案例,讲的挺清楚的。mockito加junit搞定单元测试
  2. Java单元测试神器之Mockito - 掘金
  3. 这篇很全:Mockito测试框架入门与使用
  4. 对mock基本概念的介绍挺好的:单元测试 - Mockito 详解
  5. 单元测试 - Mockito 详解

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;