基于 Mockito 框架的 Mock 测试

1.什么是 Mock 测试

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

下面是一个例子:

当我们需要测试 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 注解的被测对象中。

java 复制代码
@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 指定实现自定义逻辑的插桩
java 复制代码
@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 的另一个作用是对没有返回值的方法进行验证,证明其至少是被调用过的。

java 复制代码
@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断言

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

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

java 复制代码
Assertions.assertDoesNotThrow(() -> userServiceImplUnderTest.add("张三","25"));

4.基本思路

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

5.参考

  1. 视频,有案例,讲的挺清楚的。mockito加junit搞定单元测试
  2. Java单元测试神器之Mockito - 掘金
  3. 这篇很全:Mockito测试框架入门与使用
  4. 对mock基本概念的介绍挺好的:单元测试 - Mockito 详解
  5. 单元测试 - Mockito 详解
相关推荐
星蓝_starblue15 小时前
单元测试(UT,C++版)经验总结(gtest+gmock)
单元测试
栗子~~16 小时前
集成 jacoco 插件,查看单元测试覆盖率
缓存·单元测试·log4j
666和7771 天前
C#的单元测试
开发语言·单元测试·c#
明月看潮生1 天前
青少年编程与数学 02-004 Go语言Web编程 20课题、单元测试
开发语言·青少年编程·单元测试·编程与数学·goweb
程序猿000001号3 天前
探索Python的pytest库:简化单元测试的艺术
python·单元测试·pytest
星蓝_starblue4 天前
单元测试(C++)——gmock通用测试模版(个人总结)
c++·单元测试·log4j
whynogome4 天前
单元测试使用记录
单元测试
字节程序员4 天前
使用JUnit进行集成测试
jmeter·junit·单元测试·集成测试·压力测试
love静思冥想5 天前
Java 单元测试中 JSON 相关的测试案例
java·单元测试·json
乐闻x6 天前
如何使用 TypeScript 和 Jest 编写高质量单元测试
javascript·typescript·单元测试·jest