使用 (Power)Mockito 跟踪本地对象状态和中断流?

Track local object state and interrupt flow with (Power)Mockito?

我正在对 Spring 引导微服务进行单元测试。我想使用Mockito在某个过程中调用方法时抛出异常,以便提高单元测试覆盖率。问题是这个方法(让我们称它为 doB())被一个对象调用,该对象只存在于方法的本地范围(B),由静态最终工厂对象(A ).

我想按照以下方式做一些事情:

doThrow(new RuntimeException()).when(mockedB).doB(anyString());

这样我就可以断言 fooResult returns null 并因此通过覆盖 catch 异常案例来提高测试覆盖率。

但是,是否有可能以有用的方式创建 mockedB?如果是,怎么做?

我尝试了 mock()spy() 的各种组合,诚然收效甚微。我是使用 Mockito 的新手,但我很确定问题的关键是因为如果我只是模拟 Foo 那么我将无法看到 AB 里面做事, 但试图模拟或监视 AB 也不起作用,因为它们与 [=30= 内部创建的 AB 不同]. A final 和 static 在这里也可能对我没有任何好处。

作为示例,除了最基本的功能外,我几乎删除了所有功能。我正在测试的 class 是 Foo,它在内部使用 AB

这里是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() 到 return null。这不起作用,也不会抛出异常。

我觉得尝试这个是不好的做法。更好的方法可能是更改方法,以便我可以传入我自己的 A 对象,以便我可以观察它。但是,我们只能说我无法更改任何源代码。这甚至可能吗?

实际上,我在输入此内容时刚刚找到了一个解决方案,所以我想我会继续分享它。耶!

答案归功于 this post

Since Mocking cannot handle final, instead what we end up doing is hacking into the root of the field itself. When we use the Field manipulations (reflection), we are looking for the specific variable inside of a class/object. Once Java finds it we get the "modifiers" of it, which tell the variable what restrictions/rules it has like final, static, private, public, etc. We find the right variable, and then tell the code that it is accessible which allows us to change these modifiers. Once we have changed the "access" at the root to allow us to manipulate it, we are toggling off the "final" part of it. We then can change the value and set it to whatever we need.

使用他们的 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);
}

当然,如果有人有更好、更简单的方法来做到这一点,我会很乐意接受这个答案。