Java Springboot 后端使用Mockito库进行单元测试流程

目录

  • [1 为什么要使用mock进行单元测试](#1 为什么要使用mock进行单元测试)
  • [2 使用mock的注意点](#2 使用mock的注意点)
  • [3 mock使用流程](#3 mock使用流程)
    • [3.1 测试前配置](#3.1 测试前配置)
    • [3.2 注入待测试类并模拟其中使用的变量](#3.2 注入待测试类并模拟其中使用的变量)
      • [3.2.1 模拟成员变量](#3.2.1 模拟成员变量)
      • [3.2.2 模拟静态对象](#3.2.2 模拟静态对象)
      • [3.2.3 模拟普通变量](#3.2.3 模拟普通变量)
    • [3.3 打桩模拟方法调用行为](#3.3 打桩模拟方法调用行为)
      • [3.3.1 非静态方法打桩](#3.3.1 非静态方法打桩)
      • [3.3.2 静态方法打桩](#3.3.2 静态方法打桩)
      • [3.3.3 Maven test静态模拟报错问题](#3.3.3 Maven test静态模拟报错问题)
    • [3.4 断言判断测试结果](#3.4 断言判断测试结果)

1 为什么要使用mock进行单元测试

如果使用SpringbootTest进行单元测试,那么将启动spring服务,生成容器和bean并加载所有外部资源或依赖,当工程量很大时启动非常费时,显然为了一个小小的单元测试启动整个项目有一些大动干戈。

此外,有时部分外部依赖无法获取(如数据库连接、缓存或接口获取有问题),而单元测试旨在测试某个类具体的方法,测试的是该方法的功能逻辑,不应关注其依赖的对象,以防测试时各级方法互相调用太深(调用的方法应该在它自己的单元测试中进行测试),相当于只关注测试方法的宽度而不在意它的深度。

因此,对于要测试的方法中使用到的外部类或方法,可以考虑使用模拟对象来模拟它们的调用并自定义好返回的值,这样能够在不执行真实调用方法的同时让单元测试正常进行,这就是mock的原理。使用mock可以很好的将要测试的方法和它的依赖分隔开,降低了模块间的耦合,非常适合外部资源较难构造或方法调用太深的场景。

2 使用mock的注意点

  • 通过mock进行单元测试时并未启动spring,那么不会加载容器并生成bean,就无法通过自动注入的方式获取依赖,需要使用mock注解
  • 如果测试的相关功能与spring有关(比如AOP),那么还是需要使用SpringbootTest
  • 启用mock时可以看成启动了普通的java项目,在普通项目中能运行的代码在mock中也能运行
  • 单元测试时并非所有调用的外部方法或使用到的外部类都需要mock,如实体类可以直接new,工具类方法可以直接运行(不存在外部依赖的工具类,比如用来进行数据格式处理的工具类,通常调用其静态方法),不需要使用mock

3 mock使用流程

3.1 测试前配置

使用mock首先需要在pom文件中导入相关依赖,然后可以使用@Mock或Mockito库的mock方法来生成模拟对象,如果使用@Mock则需要先添加以下注解代码:

  1. 在测试类前添加@RunWith(MockitoJUnitRunner.class)@ExtendWith(MockitoExtension.class)
  2. 在测试类内添加如下方法:
java 复制代码
//测试前准备,可以在这里进行一些基础配置
@BeforeEach
    void setUp(){
        MockitoAnnotations.openMocks(this); //开启mock,搭配@Mock使用
}

3.2 注入待测试类并模拟其中使用的变量

若需要测试的类为UserService,那么在它的测试类UserServiceTest中需要使用注解@InjectMocks注入UserService的实例,该注解的作用是自动创建并注入一个类的实例,同时将标记为@Mock的依赖注入到该实例中,注意@InjectMocks只能注入class。

java 复制代码
@InjectMocks
UserService userService;

3.2.1 模拟成员变量

待测试类的成员变量需要使用@Mock注解注入 ,无法通过Mockito.mock()方法生成模拟对象,mock()方法生成的成员模拟对象在执行时会报空指针错误,并未真正生成模拟对象。

UserService中有成员变量UserMapper,那么使用如下代码注入:

java 复制代码
@Mock
UserMapper

简单来说就是将UserService里面的所有成员变量直接复制到Test里,然后将@Autowired改成@Mock即可。

3.2.2 模拟静态对象

当代码中需要调用某个类的静态方法时,需要使用mockStatic()方法生成一个模拟静态对象,如被测方法中若存在如下代码:

java 复制代码
DictUtil.getIns("A_END"); //DictUtil类中存在一些外部依赖,因此需要模拟

那么首先需要生成DictUtil的模拟静态对象:

java 复制代码
mockStatic(DictUtil.class);  //上下两种写法都可以
MockedStatic<DictUtil> dictUtilMockedStatic = mockStatic(DictUtil.class);

3.2.3 模拟普通变量

对于代码中生成的一些中间变量,而在后期又需要使用这些变量进行方法调用时,需要先使用mock将中间变量模拟出,如被测方法中若存在代码:

java 复制代码
UserCache.getInstance().getName("12345");

那么需要先模拟出UserCache的一个实例(打桩时不能对连续调用的方法同时打桩,需先生成中间变量 ),此时可以使用@Mock或者mock方法:

java 复制代码
// 1. 当成测试类成员变量注入
@Mock
UserCache userCache;
// 2. 在测试方法中直接模拟
UserCache userCache = Mockito.mock(UserCache.class);

3.3 打桩模拟方法调用行为

3.3.1 非静态方法打桩

打桩即对调用的方法模拟传参并设置返回值 ,参数个数和返回值需要与原方法对应。打桩之后不会真正去调用,会忽略真实方法的执行结果,一般使用Mockito.when().thenReturn()进行打桩。

若被测类方法中存在代码:

java 复制代码
User user = UserMapper.selectById("0000");

那么测试方法中的打桩可以是:

java 复制代码
//实体类不需要mock,可以直接生成,然后使用setter方法赋值
User user = new User();
// 1. 固定了打桩参数为"0000",源代码调用中参数为非"0000"时打桩失败
Mockito.when(UserMapper.selectById("0000")).thenReturn(user)
// 2. 为了代码的健壮性,通常使用Mockito.anyxxx()方法来接收参数,根据xxx的不同来指定不同的类型,只要是该类型的任意参数都可以打桩
Mockito.when(UserMapper.selectById(Mockito.anyString())).thenReturn(user)

3.3.2 静态方法打桩

以原代码UserCache.getInstance().getName("12345");为例,静态方法打桩有两种写法:

1) 普通写法

java 复制代码
UserCache userCache = Mockito.mock(UserCache.class);
mockStatic(UserCache.class);  
Mockito.when(UserCache.getInstance()).thenReturn(userCache);
Mockito.when(userCache.getName(Mockito.anyString())).thenReturn("TestName");

2) lambda写法-无参静态方法调用

java 复制代码
UserCache userCache = Mockito.mock(UserCache.class);
MockedStatic<UserCache> userCacheMockedStatic = mockStatic(UserCache.class);
//1. 当静态方法无参时,可以使用::进行方法调用
userCacheMockedStatic.when(UserCache::getInstance).thenReturn(userCache);
//2. 也可以使用lambda写法,使用.进行调用
userCacheMockedStatic.when(() -> UserCache.getInstance()).thenReturn(userCache);
//3. 不能使用如下写法:
//userCacheMockedStatic.when(UserCache.getInstance()).thenReturn(userCache);
Mockito.when(userCache.getName(Mockito.anyString())).thenReturn("TestName");

3) lambda写法-有参静态方法调用

假如上述getInstance方法有一个int参数,那么可以通过1)中的普通写法进行打桩,或者使用2)-2的方式进行打桩,然后使用Mockito.anyString()传参,但是不能使用2)-1或2)-3的写法。

3.3.3 Maven test静态模拟报错问题

当单独运行一个Test时没有问题,但是当集体执行Test时会出现如下报错:

'static mocking is already registered in the current thread To create a new mock, the existing static mock registration must be deregistered'

这是因为有两个以上的测试方法都使用了同一个静态mock对象,如两个Test中都有MockedStatic<UserCache> userCacheMockedStatic = mockStatic(UserCache.class),而静态mock对象不允许公用,因此需要在每个静态mock对象执行后对其进行关闭,下一个方法才能继续使用它,故需要在每个Test末尾中写:userCacheMockedStatic.close()

由于MockedStatic<T>的父接口继承了AutoCloseable接口,因此可以使用 try-resources语句实现资源的自动关闭:

java 复制代码
try(
	// mockStatic(UserCache.class);  //java8不支持这种写法
	MockedStatic<UserCache> userCacheMockedStatic = mockStatic(UserCache.class);
	// ......其他静态模拟对象声明
){
    //其他测试代码
}

也可以选择用try-catch-finally在finally语句里手动关闭:

java 复制代码
try(
	MockedStatic<UserCache> userCacheMockedStatic = mockStatic(UserCache.class);
	// ......其他测试代码
)finally{
    userCacheMockedStatic.close();
}

3.4 断言判断测试结果

要检测测试的结果,需要使用断言Assertions进行判断,根据实际测试要求来:

java 复制代码
User user = UserService.getById("123");
User expectUsr = new User("123");
Assertions.assertEquals(expectUsr ,user)://判断测试结果user与期望的对象是否一致
Assertions.assertNotNull(user)://判断测试结果user是否非空
相关推荐
程序猿000001号1 分钟前
探索Python的pytest库:简化单元测试的艺术
python·单元测试·pytest
Yvemil72 分钟前
《开启微服务之旅:Spring Boot Web开发举例》(一)
前端·spring boot·微服务
zjw_rp3 分钟前
Spring-AOP
java·后端·spring·spring-aop
Oneforlove_twoforjob16 分钟前
【Java基础面试题033】Java泛型的作用是什么?
java·开发语言
向宇it33 分钟前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
小蜗牛慢慢爬行35 分钟前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate
星河梦瑾1 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富1 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想2 小时前
JMeter 使用详解
java·jmeter
言、雲2 小时前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库