Mockito:如果传递给mock的参数被修改了怎么办?

时间:2013-06-10 15:24:12

标签: java unit-testing pass-by-reference mockito

我们遇到了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。这不是真正的代码,所以请不要问为什么我们创建一个列表然后清除它等。在实际代码中我们真的需要做类似的事情: - )。

1 个答案:

答案 0 :(得分:12)

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

首先,测试类:

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"));
}

当然,能够编写这样的测试需要你能够注入你的“外部对象”足够的状态,以便测试有意义......这里相对容易。