测试场景:
mockito如何测试方法内部被new出来的文件类
假如你有类似下面这样一个要测试的方法,如果直接测试会产生不必要的垃圾文件还要清理,所以我们不需要文件操作的真实行为,那么必须使用mock的方式。
/**
* 如果文件文件不存在就创建新文件,存在直接返回true
*
* @param path
* @return
* @throws IOException
*/
@Override
public boolean createFile(String path) throws IOException {
assert path != null : "The file path cannot be empty";
File file = new File(path);
if (!file.exists()) {
return file.createNewFile();
}
return true;
}
问题描述
因为是方法内部new出来的类,所以无法直接通过使用mock的方式达到效果
mockito框架中提供了mockConstruction方式来模拟被new出来的类,方法的解释如下:
/**
* Creates a thread-local mock controller for all constructions of the given class.
* The returned object's {@link MockedConstruction#close()} method must be called upon completing the
* test or the mock will remain active on the current thread.
*/
使用mockConstruction的前提必须要引入mockito-inline的依赖
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
测试代码如下:
@ExtendWith(MockitoExtension.class)
class MyServiceImplTest {
@InjectMocks
private MyServiceImpl myServiceImpl;
@Test
void createFile() throws IOException {
//given
MockedConstruction<File> fileMockedConstruction = Mockito.mockConstruction(File.class, (mock, context) -> {
//只要被测试的类存在new File()这样的操作,就会调用该代码,因为会把所有的构造函数都模拟调用一遍,我们这里只要我们指定的构造函数
//我们在new File的时候传递一个path,只要这里的参数和传递的参数相同,就进行接下来的mock行为
if (!"path".equals(context.arguments().get(0))) {
return;
}
Mockito.when(mock.exists()).thenReturn(false);
Mockito.when(mock.createNewFile()).thenReturn(true);
});
//when
boolean result = myServiceImpl.createFile("path");
//then
fileMockedConstruction.close();
assertTrue(result);
}
}
但是上面的代码执行后会报错,而且是一个栈溢出的错误
原因分析:
为什么上面的代码运行后会报栈溢出错误呢
主要是因为在运行测试的时候会加载很多类,就是把用到的类加载到jvm中,而类的加载是根据文件操作来进行的,找到指定的类文件然后读取加载到jvm中,在读取类的过程中会存在一些new File()的操作来读取类,但是我们已经给当前线程mockConstruction,所以只要遇到一些要被加载的类都会去执行,而且所有的构造函数都会被mock一遍,一个测试方法启动运行起来要加载很多类,还有自己业务方法里面可能也会存在,就会导致mock的对象越来越多,从而导致出现了栈溢出错误。
解决方案:
把无关的mock对象从当前线程中清理掉即可
代码如下:
@ExtendWith(MockitoExtension.class)
class MyServiceImplTest {
@InjectMocks
private MyServiceImpl myServiceImpl;
@Test
void createFile() throws IOException {
//given
MockedConstruction<File> fileMockedConstruction = Mockito.mockConstruction(File.class, (mock, context) -> {
//只要被测试的类存在new File()这样的操作,就会调用该代码,因为会把所有的构造函数都模拟调用一遍,我们这里只要我们指定的构造函数
//我们在new File的时候传递一个path,只要这里的参数和传递的参数相同,就进行接下来的mock行为
if (!"path".equals(context.arguments().get(0))) {
//清理掉无用的mock,底层是其实是一个弱引用的map存放了所有的mock对象
Mockito.framework().clearInlineMock(mock);
return;
}
Mockito.when(mock.exists()).thenReturn(false);
Mockito.when(mock.createNewFile()).thenReturn(true);
});
//when
boolean path = myServiceImpl.createFile("path");
//then
fileMockedConstruction.close();
assertTrue(path);
}
}
特别注意的是这种文件操作建议不要去mock,虽然上面的操作可以解决这种问题,但是如果自己的业务代码比较多加载的类就会比较多可能会存在未知问题,所以这种new File()操作建议使用工具类去new出来,业务代码屏蔽掉new的细节这也比较符合java的开闭原则规范,同时在做单元测试的时候只需要mock工具类就行,这样就不会存在上面说的这些问题