Java Springboot 后端使用Mockito库进行单元测试流程分析
作者:Bling_
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
则需要先添加以下注解或代码:
- 在测试类前添加
@RunWith(MockitoJUnitRunner.class)
或@ExtendWith(MockitoExtension.class)
- 在测试类内添加如下方法:
//测试前准备,可以在这里进行一些基础配置 @BeforeEach void setUp(){ MockitoAnnotations.openMocks(this); //开启mock,搭配@Mock使用 }
3.2 注入待测试类并模拟其中使用的变量
若需要测试的类为UserService
,那么在它的测试类UserServiceTest
中需要使用注解@InjectMocks
注入UserService
的实例,该注解的作用是自动创建并注入一个类的实例,同时将标记为@Mock
的依赖注入到该实例中,注意@InjectMocks
只能注入class。
@InjectMocks UserService userService;
3.2.1 模拟成员变量
待测试类的成员变量需要使用@Mock
注解注入,无法通过Mockito.mock()
方法生成模拟对象,mock()方法生成的成员模拟对象在执行时会报空指针错误,并未真正生成模拟对象。
若UserService
中有成员变量UserMapper
,那么使用如下代码注入:
@Mock UserMapper
简单来说就是将UserService
里面的所有成员变量直接复制到Test
里,然后将@Autowired
改成@Mock
即可。
3.2.2 模拟静态对象
当代码中需要调用某个类的静态方法时,需要使用mockStatic()
方法生成一个模拟静态对象,如被测方法中若存在如下代码:
DictUtil.getIns("A_END"); //DictUtil类中存在一些外部依赖,因此需要模拟
那么首先需要生成DictUtil
的模拟静态对象:
mockStatic(DictUtil.class); //上下两种写法都可以 MockedStatic<DictUtil> dictUtilMockedStatic = mockStatic(DictUtil.class);
3.2.3 模拟普通变量
对于代码中生成的一些中间变量,而在后期又需要使用这些变量进行方法调用时,需要先使用mock将中间变量模拟出,如被测方法中若存在代码:
UserCache.getInstance().getName("12345");
那么需要先模拟出UserCache
的一个实例(打桩时不能对连续调用的方法同时打桩,需先生成中间变量),此时可以使用@Mock
或者mock方法:
// 1. 当成测试类成员变量注入 @Mock UserCache userCache; // 2. 在测试方法中直接模拟 UserCache userCache = Mockito.mock(UserCache.class);
3.3 打桩模拟方法调用行为
3.3.1 非静态方法打桩
打桩即对调用的方法模拟传参并设置返回值,参数个数和返回值需要与原方法对应。打桩之后不会真正去调用,会忽略真实方法的执行结果,一般使用Mockito.when().thenReturn()
进行打桩。
若被测类方法中存在代码:
User user = UserMapper.selectById("0000");
那么测试方法中的打桩可以是:
//实体类不需要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) 普通写法
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写法-无参静态方法调用
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语句实现资源的自动关闭:
try( // mockStatic(UserCache.class); //java8不支持这种写法 MockedStatic<UserCache> userCacheMockedStatic = mockStatic(UserCache.class); // ……其他静态模拟对象声明 ){ //其他测试代码 }
也可以选择用try-catch-finally在finally语句里手动关闭:
try( MockedStatic<UserCache> userCacheMockedStatic = mockStatic(UserCache.class); // ……其他测试代码 )finally{ userCacheMockedStatic.close(); }
3.4 断言判断测试结果
要检测测试的结果,需要使用断言Assertions
进行判断,根据实际测试要求来:
User user = UserService.getById("123"); User expectUsr = new User("123"); Assertions.assertEquals(expectUsr ,user)://判断测试结果user与期望的对象是否一致 Assertions.assertNotNull(user)://判断测试结果user是否非空
到此这篇关于Java Springboot 后端使用Mockito库进行单元测试流程的文章就介绍到这了,更多相关Springboot Mockito单元测试内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!