10

In Mockito we have a situation where the capture of a list doesn't return the expected result. Test case:

  1. We add "Pip" to a list
  2. We capture the list
  3. We add "Sok" to the list.

In our assert we only expect "Pip" to be there but "Sok" is there too. We think this is incorrect because at the time of capture "Sok" was not in the list.

java.lang.AssertionError:
Expected :[Pip]
Actual :[Pip, Sok]

  • Has anyone got a solution for this?
  • Is this a bug or a feature in Mockito?
  • Why does Mockito keep a reference to the list and not make a copy of the list a capture time?

Here is the test case:

@RunWith(MockitoJUnitRunner.class)
public class CaptureTest {

    @Captor
    private ArgumentCaptor<List> listCapture;

    @Mock
    private ListPrinter listPrinter;

    private TestClass testClass;

    @Before
    public void setUp() {
        testClass = new TestClass(listPrinter);
    }

    @Test
    public void testCapture() {
        testClass.simulateFailSituation();
        verify(listPrinter).printList(listCapture.capture());
        // THIS FAILS: Expected:[Pip],  Actual:[Pip, Sok]
        assertEquals(Collections.singletonList("Pip"), listCapture.getValue());
    }

    public class TestClass {

        private List list = new ArrayList();
        private ListPrinter listPrinter;

        public TestClass(ListPrinter listPrinter) {
            this.listPrinter = listPrinter;
        }

        private void simulateFailSituation() {
            list.add("Pip");
            listPrinter.printList(list);
            list.add("Sok");
        }
    }

    public interface ListPrinter {
        void printList(List list);
    }
  }
Nos
  • 1,024
  • 1
  • 8
  • 14

4 Answers4

8

It might sound like an amazing feature, but then think about it that way: If it was making a copy, where should it stop? You could possibly capture some object that has many references to other objects and you might end up making a deep copy of nearly all the objects in your JVM instance.

That would be a serious performance hit, so I kinda understand why.

So, there are two approaches you could choose from:

  • Use immutable objects where applicable. Other than being easier to test, it also makes the code easier to read and debug.
  • Test the value instantly upon call, rather than capturing the reference. For void methods you could use the doAnswer method with Answer<Void>, test it there or make a copy. By the way this is noting new, see How to make mock to void methods with mockito.

I find it a lot more powerful than verify. In your case, the doAnswer could look like that:

doAnswer(invocation -> {
    assertEquals(Collections.singletonList("Pip"), invocation.getArguments()[0]);
    return null;
}).when(listPrinter).printList(Matchers.anyList());
Vlasec
  • 5,500
  • 3
  • 27
  • 30
4

To agree with Vlasec's answer, it wouldn't make sense for Mockito to deep copy by default. It won't be able to tell which objects are immutable value objects (like String), which are easy to copy (ArrayList?), which should absolutely not be copied (Thread?), and so forth. However, you can use an Answer to make your own copy when the method is called.

@Test
public void testCapture() {
    // from memory - may need warnings suppressed or different casts/generics
    List<String> listSnapshot = new ArrayList<>();
    doAnswer(invocation -> {
        listSnapshot.addAll((List) invocation.getArguments()[0]);
        return null;
    }).when(listPrinter).printList(any());
    testClass.simulateFailSituation();
    listCapture.capture());

    assertEquals(Collections.singletonList("Pip"), listSnapshot);
}

Though Vlasec's update shows a way to perform a validation within an Answer, I prefer this manual capture as it approximates ArgumentCaptor better and fits more into "given/when/then" or "expect/perform/verify" test formats. Also, a failed assertion mid-test will cause a Mockito failure while your system under test is on the stack, possibly leading to a less-clear failure than if the verification is after your tested method returns.

Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
0

One way to deal with this is to call

      doAnswer(invocationOnMock -> {return null;}).when(listPrinter).printList(any());

And then in the hook do your own capture with deep copy, or verify in place.

-1

This is happening because Mockito maintains a reference to the object passed as a parameter.

You can try this.

private void simulateFailSituation() {
     list.add("Pip");
     listPrinter.printList(new ArrayList(list));
     list.add("Sok");
}

This way you would also make sure printList doesn't mutate your list.

alayor
  • 4,537
  • 6
  • 27
  • 47
  • 2
    Copying stuff like that in production code just because of a test? I don't consider this a great idea from that point of view. – Vlasec Oct 25 '17 at 16:02
  • 1
    Tests are more important than production code. If tests are making your production code change, that means there is a design issue and it should be solved. – alayor Oct 25 '17 at 16:25
  • 2
    I saw ugly hacks made to make code testable. And this defensive copy fits into that category as well. While I agree making changes to production code because of tests can make it better, it can also make it worse, like in this case. – Vlasec Oct 26 '17 at 09:03
  • Also, in case you wanted to make sure the printer doesn't mutate the list, you could simply call Collections.unmodifiableList, resulting in a wrapper rather than a copy of the collection. Yet neither method on its own prevents mutation if the objects inside are mutable (in case of String it works, but in general it doesn't have to). – Vlasec Oct 26 '17 at 10:10