10

我们在 Mockito 上遇到了非常讨厌的问题。

代码:

public class Baz{
    private Foo foo;
    private List list;

    public Baz(Foo foo){
        this.foo = foo;
    }

    public void invokeBar(){
        list = Arrays.asList(1,2,3);
        foo.bar(list);
        list.clear();
    }

}


public class BazTest{

   @Test
   void testBarIsInvoked(){
        Foo mockFoo = mock(Foo.class);
        Baz baz = new Baz(mockFoo);           
        baz.invokeBar();
        verify(mockFoo).bar(Arrays.asList(1,2,3));
   }
}

这会导致错误消息,如:

Arguments are different! Wanted: 
foo.bar([1,2,3]); 
Actual invocation has different arguments:
foo.bar([]);

刚才发生了什么:

Mockito 记录对 的引用list不是副本list,因此在上面的代码中,Mockito 验证修改后的版本(空列表,[]),而不是调用期间实际传递的版本([1,2,3])!

问题:

除了像下面那样做一个防御性副本(这实际上有帮助,但我们不喜欢这个解决方案)之外,是否有任何优雅而干净的解决方案来解决这个问题?

   public void fun(){
        list = Arrays.asList(1,2,3);
        foo.bar(new ArrayList(list));
        list.clear();
    }

我们不想修改正确的生产代码并降低其性能,只是为了解决测试的技术问题。

我在这里问这个问题是因为这似乎是 Mockito 的常见问题。或者我们只是做错了什么?

PS。这不是一个真实的代码,所以请不要问我们为什么要创建一个列表然后清除它等等。在真实的代码中,我们确实需要做类似的事情:-)。

4

1 回答 1

15

这里的解决方案是使用自定义答案。两个代码示例:第一个是使用的测试类,第二个是测试。

一、测试类:

private interface Foo
{
    void bar(final List<String> list);
}

private static final class X
{
    private final Foo foo;

    X(final Foo foo)
    {
        this.foo = foo;
    }

    void invokeBar()
    {
        // Note: using Guava's Lists here
        final List<String> list = Lists.newArrayList("a", "b", "c");
        foo.bar(list);
        list.clear();
    }
}

进入测试:

@Test
@SuppressWarnings("unchecked")
public void fooBarIsInvoked()
{
    final Foo foo = mock(Foo.class);
    final X x = new X(foo);

    // This is to capture the arguments with which foo is invoked
    // FINAL IS NECESSARY: non final method variables cannot serve
    // in inner anonymous classes
    final List<String> captured = new ArrayList<String>();

    // Tell that when foo.bar() is invoked with any list, we want to swallow its
    // list elements into the "captured" list
    doAnswer(new Answer()
    {
        @Override
        public Object answer(final InvocationOnMock invocation)
            throws Throwable
        {
            final List<String> list
                = (List<String>) invocation.getArguments()[0];
            captured.addAll(list);
            return null;
        }
    }).when(foo).bar(anyList());

    // Invoke...
    x.invokeBar();

    // Test invocation...
    verify(foo).bar(anyList());

    // Test arguments: works!
    assertEquals(captured, Arrays.asList("a", "b", "c"));
}

当然,能够编写这样的测试需要您能够将足够的状态注入到您的“外部对象”中,这样​​测试才有意义……这里相对容易。

于 2013-06-10T15:35:47.073 回答