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

Posted

技术标签:

【中文标题】使用 (Power)Mockito 跟踪本地对象状态和中断流程?【英文标题】:Track local object state and interrupt flow with (Power)Mockito? 【发布时间】:2019-05-22 06:22:42 【问题描述】:

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

我想做一些类似的事情:

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

这样我就可以断言fooResult 返回null,从而通过覆盖捕获异常情况来提高测试覆盖率。

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

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

例如,除了最基本的功能外,我几乎删除了所有内容。我正在测试的类是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);
  



正如我之前所说,我希望在调用doB() 时触发模拟,导致doB() 返回null。这不起作用,也不会抛出异常。

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

【问题讨论】:

考虑将构造函数添加到Foo,以便可以将A 的Spring bean 注入FooFoo.localA 可能不需要是静态的,但它仍然是最终的是个好主意。这通过模拟A 并使用构造函数将其放入Foo 来简化单元测试。模拟的A 可以设置为抛出异常,而无需对B 做任何事情。 【参考方案1】:

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

答案转给this post:

由于 Mocking 无法处理 final,因此我们最终要做的是侵入字段本身的根。当我们使用字段操作(反射)时,我们正在寻找类/对象内部的特定变量。一旦 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);

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

【讨论】:

以上是关于使用 (Power)Mockito 跟踪本地对象状态和中断流程?的主要内容,如果未能解决你的问题,请参考以下文章

使用 mockito 验证对象属性值

Mockito - 验证对象根本没有被调用

从已发布的 Power BI 视觉对象中抓取数据

Mockito:将InOrder与间谍对象一起使用

手把手教你 Mockito 的使用

手把手教你 Mockito 的使用