Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mockito: what if argument passed to mock is modified?

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 :-).

like image 791
Piotr Sobczyk Avatar asked Jun 10 '13 15:06

Piotr Sobczyk


People also ask

Does Mockito verify use equals?

Mockito verifies argument values in natural java style: by using an equals() method.

What does stubbing mean in Mockito?

A stub is a fake class that comes with preprogrammed return values. It's injected into the class under test to give you absolute control over what's being tested as input. A typical stub is a database connection that allows you to mimic any scenario without having a real database.

Are Mockito mocks thread safe?

Yes, they are.

Does Mockito mock call real method?

Mockito allows us to partially mock an object. This means that we can create a mock object and still be able to call a real method on it. To call a real method on a mocked object we use Mockito's thenCallRealMethod().


1 Answers

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.

like image 167
fge Avatar answered Oct 20 '22 07:10

fge