12

We came across really nasty problem with Mockito.

Code:

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

This causes error message like:

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

What just happened:

Mockito records reference to list rather than copy of list, so in the code above Mockito verifies against modified version (empty list, []) instead to the one actually passed during invocation ([1,2,3])!

Question:

Is there any elegant and clean solution to this problem other than doing a defensive copy like below (which actually helps but we don't like this solution)?

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

We don't want to modify correct production code and reduce its performance only to fix technical problem with test.

I'm asking this question here because it seems to be possibly common problem with Mockito. Or we just do something wrong?

PS. This is not a real code so please don't ask why we create a list and then clear it etc. In real code we have a real need to do something similar :-).

Piotr Sobczyk
  • 6,443
  • 7
  • 47
  • 70
  • Do you actually verify on a _different instance_ of the same argument class? – fge Jun 10 '13 at 15:33
  • @fge Yes, I have no access to the original instance in test code so I need to create a new instance of argument in test with the expected content. – Piotr Sobczyk Jun 10 '13 at 15:37

1 Answers1

15

The solution here is to use a customized answer. Two code samples: the first is the test classes used, the second is the test.

First, the test classes:

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

On to the test:

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

Of course, being able to write such a test requires that you are able to inject into your "outer object" sufficient state so that the test is meaningful... Here it is relatively easy.

fge
  • 119,121
  • 33
  • 254
  • 329
  • Thanks for answer, but I'm pretty sure that by default `eq` matcher is used, not `same`. I updated my question to clarify it. As you see, when I put a copy of `list` the test will pass (that means, `eq` matcher is used). But this solution doesn't satisfy us. – Piotr Sobczyk Jun 10 '13 at 20:23
  • Ah OK. Then I have another solution! – fge Jun 10 '13 at 20:41
  • 2
    Glad it works ;) I have used mockito for quite some time but it's the first time I used `doAnswer()` this way :p – fge Jun 11 '13 at 07:18