目录
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)
- 在测试类内添加如下方法:
//测试前准备,可以在这里进行一些基础配置@BeforeEachvoidsetUp(){MockitoAnnotations.openMocks(this);//开启mock,搭配@Mock使用}
3.2 注入待测试类并模拟其中使用的变量
若需要测试的类为
UserService
,那么在它的测试类
UserServiceTest
中需要使用注解
@InjectMocks
注入
UserService
的实例,该注解的作用是自动创建并注入一个类的实例,同时将标记为
@Mock
的依赖注入到该实例中,注意
@InjectMocks
只能注入class。
@InjectMocksUserService userService;
3.2.1 模拟成员变量
待测试类的**成员变量需要使用
@Mock
注解注入**,无法通过
Mockito.mock()
方法生成模拟对象,mock()方法生成的成员模拟对象在执行时会报空指针错误,并未真正生成模拟对象。
若
UserService
中有成员变量
UserMapper
,那么使用如下代码注入:
@MockUserMapper
简单来说就是将
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. 当成测试类成员变量注入@MockUserCache 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 =newUser();// 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 =newUser("123");
Assertions.assertEquals(expectUsr ,user)://判断测试结果user与期望的对象是否一致Assertions.assertNotNull(user)://判断测试结果user是否非空
版权归原作者 Bling_ 所有, 如有侵权,请联系我们删除。