JUnit与Mockito:如何模拟带Mock参数实例化的类?
问题场景
你的测试代码与被测试类如下:
public class TestClass { @Test public void testSomething() { SomethingConfig somethingConfigMock = mock(SomethingConfig.class); final ClassBeingTested cbt = new ClassBeingTested( parameter1, parameter2, somethingConfigMock ); cbt.getResults(); } } public class ClassBeingTested { public ClassBeingTested(parameter1, parameter2, somethingConfig) { this.parameter1 = parameter1; this.parameter2 = parameter2; this.somethingConfig = somethingConfig; // 其他初始化逻辑 } public Results getResults() { // 其他业务逻辑 OtherClassToTest otherClassToTest = new OtherClassToTest(somethingConfig); String s = otherClassToTest.getSomeString(); // 其他业务逻辑 } }
你希望OtherClassToTest通过somethingConfigMock实例化后,getSomeString()等方法能返回指定结果,尝试的写法如下:
when(new OtherClassToTest(somethingConfigMock)).thenReturn(otherClassToTestMock); when(otherClassToTestMock.getSomeString()).thenReturn("genericString");
但该写法无法生效,且你不想对现有代码做大量重构;之前仅mocksomethingConfigMock时,OtherClassToTest内部会出现空指针异常。
思路分析
你的写法存在错误:Mockito默认无法直接拦截new关键字创建对象的行为,因为new会生成一个全新的实例,和你预先mock的otherClassToTestMock完全无关,所以when(new...)的逻辑不会对实际运行的代码产生影响。
无需大量重构的解决方案
方案1:抽离对象创建逻辑,用Spy模拟(推荐)
仅需对ClassBeingTested做微小修改,把创建OtherClassToTest的逻辑抽成一个protected辅助方法:
public class ClassBeingTested { // 原有成员变量、构造方法不变 // 抽离对象创建逻辑 protected OtherClassToTest createOtherClassToTest(SomethingConfig config) { return new OtherClassToTest(config); } public Results getResults() { // 其他业务逻辑 // 调用辅助方法创建对象 OtherClassToTest otherClassToTest = createOtherClassToTest(somethingConfig); String s = otherClassToTest.getSomeString(); // 其他业务逻辑 } }
然后在测试类中,用Mockito的spy包装被测试实例,模拟这个辅助方法的返回:
@Test public void testSomething() { SomethingConfig somethingConfigMock = mock(SomethingConfig.class); OtherClassToTest otherClassToTestMock = mock(OtherClassToTest.class); // 设置mock对象的方法返回值 when(otherClassToTestMock.getSomeString()).thenReturn("genericString"); // 创建被测试类的Spy实例(保留原有逻辑,仅指定方法被模拟) ClassBeingTested cbt = spy(new ClassBeingTested(parameter1, parameter2, somethingConfigMock)); // 让辅助方法返回我们预先mock好的对象 doReturn(otherClassToTestMock).when(cbt).createOtherClassToTest(somethingConfigMock); cbt.getResults(); // 后续添加断言逻辑 }
这种修改仅新增一个辅助方法,完全不影响原有业务逻辑,是最轻量、最易维护的方案。
方案2:用PowerMock拦截new操作(无需修改被测试类)
如果你完全不想修改被测试类,可以使用PowerMock扩展Mockito的能力,直接拦截new关键字的行为。需要先引入PowerMock的相关依赖,然后按以下方式编写测试:
// 必须添加注解,指定测试Runner和需要拦截的类 @RunWith(PowerMockRunner.class) @PrepareForTest(ClassBeingTested.class) public class TestClass { @Test public void testSomething() { SomethingConfig somethingConfigMock = mock(SomethingConfig.class); OtherClassToTest otherClassToTestMock = mock(OtherClassToTest.class); when(otherClassToTestMock.getSomeString()).thenReturn("genericString"); // 拦截new OtherClassToTest(somethingConfigMock)的操作,返回mock对象 PowerMockito.whenNew(OtherClassToTest.class) .withArguments(somethingConfigMock) .thenReturn(otherClassToTestMock); ClassBeingTested cbt = new ClassBeingTested(parameter1, parameter2, somethingConfigMock); cbt.getResults(); // 后续添加断言逻辑 } }
注意:PowerMock需要特定的测试Runner和注解,会增加测试的复杂度,因此优先推荐方案1。
关于空指针异常的说明
之前仅mocksomethingConfigMock时出现NPE,是因为OtherClassToTest的构造方法或getSomeString()方法中,调用了somethingConfigMock的某些方法,但你没有为这些方法设置返回值,导致调用时返回null,进而触发空指针。使用上述两种方案后,无需关注somethingConfigMock的内部调用,直接模拟OtherClassToTest的返回即可避免该问题。
内容的提问来源于stack exchange,提问作者Max Deyo

