我正在对SpringBoot微服务进行单元测试。我想使用Mockito在某个进程中调用方法时抛出异常,这样我就可以提高单元测试的覆盖率。问题是,这个方法(我们称之为doB()
)是由一个只存在于该方法的本地作用域(B
)中的对象调用的,该对象由一个静态的最终工厂对象(A
)创建。
我想做一些类似的事情
doThrow(new RuntimeException()).when(mockedB).doB(anyString());
以便我可以断言fooResult
返回null
,从而通过覆盖catch异常情况来提高测试覆盖率。
但是,有可能以一种有用的方式创建mockedB
吗?如果是,如何?
我尝试过mock()
和spy()
的各种组合,无可否认,收效甚微。我是使用Mockito的新手,但我很确定问题的关键是,如果我只是模拟Foo,那么我将无法看到里面的A
和B
在做事情,但试图模拟或监视A
或B
也不起作用,因为它们与Foo
内部创建的A
或B
不同。A
是最终的和静态的可能对我也没有任何好处。
作为一个例子,我删除了几乎所有的基本功能。我正在测试的类是Foo
,它在内部使用A
和B
。
这里是Foo
:
public class Foo {
private static final A localA = new A();
public FooResult doFoo(String fooString) {
try {
B localB = localA.createB();
return localB.doB(fooString);
} catch (RuntimeException e) {
//exception handling here
}
return null;
}
}
这是A
:
public class A {
//unimportant internal details
private Object property;
public A() {
this(null);
}
public A(Object property) {
this.property = property;
}
public B createB() {
//assume for sake of example that this constructor
//is not easily accessible to classes other than A
return new B();
}
}
现在B
:
public class B {
public FooResult doB(String str) throws RuntimeException {
//lots of processing, yada yada...
//assume this exception is difficult to trigger just
//from input due to details out of our control
return new FooResult(str);
}
}
这是FooResult
public class FooResult {
private String fooString;
public FooResult(String str) {
this.fooString = str;
}
public String getFooString() {
return fooString;
}
}
最后,这里是测试:
@RunWith(PowerMockRunner.class)
public class FooTest {
@InjectMocks
Foo foo;
@Test
public void testDoFoo() {
String fooString = "Hello Foo";
FooResult fooResult = foo.doFoo(fooString);
assertEquals(fooResult.getFooString(), fooString);
//works fine, nothing special here
}
@Test
@PrepareForTest({Foo.class, A.class, B.class})
public void testDoFooException() throws Exception {
//magic goes here???
A mockedA = PowerMockito.mock(A.class);
B mockedB = PowerMockito.mock(B.class);
PowerMockito.when(mockedA.createB()).thenReturn(mockedB);
PowerMockito.doThrow(new RuntimeException()).when(mockedB).doB(Mockito.anyString());
FooResult fooResult = foo.doFoo("Hello Foo");
//expect doFoo() to fail and return null
assertNull(fooResult);
}
}
正如我前面所说,我希望mock在调用doB()
时触发,从而使doB()
返回null
。这不起作用,也不会引发异常。
我有一种感觉,尝试这种做法是不好的。更好的方法可能是更改方法,这样我就可以传入自己的A
对象,这样我就能观察到它。但是,我们只能说我不能更改任何源代码。这可能吗?
实际上,我在打字时刚刚找到了一个解决方案,所以我想我会继续分享它。耶!
这篇文章的答案值得称赞:
由于Mocking无法处理final,因此我们最终要做的是侵入字段本身的根。当我们使用Field操作(反射)时,我们在类/对象内部寻找特定的变量。一旦Java找到它,我们就会得到它的"修饰符",告诉变量它有什么限制/规则,如final、static、private、public等。我们找到合适的变量,然后告诉代码它是可访问的,这允许我们更改这些修饰符。一旦我们更改了根的"访问权限"以允许我们操作它,我们就会切换它的"最后"部分。然后我们可以更改值并将其设置为我们需要的任何值。
使用他们的setFinalStatic
函数,我能够成功地模拟A
并引发RuntimeException
。
以下是与助手一起进行的工作测试:
//make fields accessible for testing
private static void setFinalStatic(Field field, Object newValue) throws Exception {
field.setAccessible(true);
// remove final modifier from field
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, newValue);
}
@Test
@PrepareForTest({A.class})
public void testDoFooException() {
A mockedA = PowerMockito.mock(A.class);
B mockedB = PowerMockito.mock(B.class);
try {
setFinalStatic(Foo.class.getDeclaredField("localA"), mockedA);
} catch (Exception e) {
fail("setFinalStatic threw exception: " + e);
}
Mockito.when(mockedA.createB()).thenReturn(mockedB);
PowerMockito.doThrow(new RuntimeException()).when(mockedB).doB(Mockito.anyString());
FooResult fooResult = foo.doFoo("Hello Foo");
assertNull(fooResult);
}
当然,如果有人有更好的、不那么粗鲁的方法来做这件事,我很乐意接受这个答案。