Mockito: what if the argument passed to the layout is modified?

We ran into a very 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 an error message like:

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

What just happened:

Mockito writes the link to list instead of copying list , so in the code above Mockito checks instead of the modified version (empty list, [] ) instead of the one that was actually passed during invocation ( [1,2,3] )!

Question:

Is there an elegant and clean solution to this problem besides creating a protective copy, as shown below (which actually helps, but we don’t like it)?

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

We do not want to change the production code to the correct one and reduce its performance only to eliminate a technical problem with the test.

I ask this question here because it is possibly a common problem with Mockito. Or are we just doing something wrong?

PS. This is not real code, so please don’t ask why we create a list and then clear it, etc. In real code, we really need to do something like this :-).

+6
source share
1 answer

The solution here is to use an individual answer. Two code examples: the first is the test classes used, the second is the test.

Test classes first:

 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 Lists here final List<String> list = Lists.newArrayList("a", "b", "c"); foo.bar(list); list.clear(); } } 

In 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, the ability to write such a test requires that you can enter a sufficient state in your "external object" so that the test is significant ... Here it is relatively easy.

+11
source

Source: https://habr.com/ru/post/946971/


All Articles