Repository: mockito/mockito
Issue: when mock is called multiple times, and verify fails, the error message reports only the first invocation
Labels: please contribute

AS A diligent dev,
I WANT TO verify that a mock will eventually get called with the specified parameters,
SO THAT I can properly test my builder class

SCENARIO
I have a builder class exposing a .with(String key, String value) method.
I created a test for testing that at a certain point the method gets called with 'foo', 'valueFoo'
I'd like to follow a test pattern where 1 test case has only 1 assertion

```
builderMock.with('bar','valueBar');
builderMock.with('foo','notValueFoo');
verify(builderMock).with('foo','valueFoo');
```

WHAT'S WRONG
Verify reports only the first invocation in the test failed logs.

```
Arguments are different! Wanted: 
builderMock.with('foo','valueFoo');
at ...
Actual invocation has different arguments:
builderMock.with('bar','valueBar');
at ...
``` 

IMO should be a list of invocation for that methods, something like:

```
// Actual invocations have different arguments:
// builderMock.with('bar','valueBar');
// at ...
// builderMock.with('foo','notValueFoo');
// at ...
```

NOTE
For anybody else having this problem, I'm rethrowing the exception raised with something like this:
```
    public static ArgumentsAreDifferent createExceptionWithEnhancedVerifyLogsFor(
            Object mockedObject,
            String invokedMethod,
            ArgumentsAreDifferent ex
    ) {
        String newline = System.getProperty("line.separator");
        String wanted = ex.getExpected();
        String actualTitle = MessageFormat.format(
                "Interactions of {0}.{1}():",
                mockedObject.getClass().getCanonicalName(), invokedMethod
        );
        String actual = mockingDetails(mockedObject).getInvocations().stream()
                .filter(invocation -> invocation.getMethod().getName().equals(invokedMethod))
                .map(Object::toString)
                .collect(Collectors.joining( newline ));
        String message = String.join(newline,
                ex.getClass().getCanonicalName(),
                "Wanted: " + wanted,
                actualTitle, actual
        );
        ArgumentsAreDifferent enhancedEx = new ArgumentsAreDifferent(message, wanted, actual);
        enhancedEx.setStackTrace(ex.getStackTrace());
        return enhancedEx;
    }
```

Issue comments:
Nice report, thank you! Do you want to take a stub at fixing it? I suspect that we need to tweak the reporting, so that we print other interactions, too. We do something like that for regular verification failures (as opposed to verification failures due to mismatched arguments)
I can try and take a look at this.
hi @mockitoguy
sorry my email is bleeding with spam and I've been caught up with some deadlines
it would be an interesting piece of work that I'd like to take but I don't have the time right now for
it's going into my todo list, though
Just started working on this today. Doesn't seem like too bad of a change. I should have something to show later today or tomorrow.

Pull request review comments:

Patch that resolved the issue:
diff --git a/src/main/java/org/mockito/internal/exceptions/Reporter.java b/src/main/java/org/mockito/internal/exceptions/Reporter.java
index 1b68fbb657..30513525f4 100644
--- a/src/main/java/org/mockito/internal/exceptions/Reporter.java
+++ b/src/main/java/org/mockito/internal/exceptions/Reporter.java
@@ -299,17 +299,33 @@ private static Object locationsOf(Collection<LocalizedMatcher> matchers) {
         return join(description.toArray());
     }
 
-    public static AssertionError argumentsAreDifferent(String wanted, String actual, Location actualLocation) {
-        String message = join("Argument(s) are different! Wanted:",
-                              wanted,
-                              new LocationImpl(),
-                              "Actual invocation has different arguments:",
-                              actual,
-                              actualLocation,
-                              ""
-        );
+    public static AssertionError argumentsAreDifferent(String wanted, List<String> actualCalls, List<Location> actualLocations) {
+        if (actualCalls == null || actualLocations == null || actualCalls.size() != actualLocations.size()) {
+            throw new IllegalArgumentException("actualCalls and actualLocations list must match");
+        }
+
+
+        StringBuilder actualBuilder = new StringBuilder();
+        StringBuilder messageBuilder = new StringBuilder();
+        messageBuilder.append("\n")
+            .append("Argument(s) are different! Wanted:\n")
+            .append(wanted)
+            .append("\n")
+            .append(new LocationImpl())
+            .append("\n")
+            .append("Actual invocations have different arguments:\n");
+
+        for (int i = 0; i < actualCalls.size(); i++) {
+            actualBuilder.append(actualCalls.get(i))
+                .append("\n");
+
+            messageBuilder.append(actualCalls.get(i))
+                .append("\n")
+                .append(actualLocations.get(i))
+                .append("\n");
+        }
 
-        return ExceptionFactory.createArgumentsAreDifferentException(message, wanted, actual);
+        return ExceptionFactory.createArgumentsAreDifferentException(messageBuilder.toString(), wanted, actualBuilder.toString());
     }
 
     public static MockitoAssertionError wantedButNotInvoked(DescribedInvocation wanted) {
diff --git a/src/main/java/org/mockito/internal/reporting/SmartPrinter.java b/src/main/java/org/mockito/internal/reporting/SmartPrinter.java
index 8da053ddf1..24d8367b00 100644
--- a/src/main/java/org/mockito/internal/reporting/SmartPrinter.java
+++ b/src/main/java/org/mockito/internal/reporting/SmartPrinter.java
@@ -8,6 +8,10 @@
 import org.mockito.invocation.Invocation;
 import org.mockito.invocation.MatchableInvocation;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
 /**
  * Makes sure both wanted and actual are printed consistently (single line or multiline)
  * <p>
@@ -16,22 +20,40 @@
 public class SmartPrinter {
 
     private final String wanted;
-    private final String actual;
+    private final List<String> actuals;
 
     public SmartPrinter(MatchableInvocation wanted, Invocation actual, Integer ... indexesOfMatchersToBeDescribedWithExtraTypeInfo) {
+        this(wanted, Collections.singletonList(actual), indexesOfMatchersToBeDescribedWithExtraTypeInfo);
+    }
+
+    public SmartPrinter(MatchableInvocation wanted, List<Invocation> allActualInvocations, Integer ... indexesOfMatchersToBeDescribedWithExtraTypeInfo) {
         PrintSettings printSettings = new PrintSettings();
-        printSettings.setMultiline(wanted.toString().contains("\n") || actual.toString().contains("\n"));
+        printSettings.setMultiline(isMultiLine(wanted, allActualInvocations));
         printSettings.setMatchersToBeDescribedWithExtraTypeInfo(indexesOfMatchersToBeDescribedWithExtraTypeInfo);
 
         this.wanted = printSettings.print(wanted);
-        this.actual = printSettings.print(actual);
+
+        List<String> actuals = new ArrayList<String>();
+        for (Invocation actual : allActualInvocations) {
+            actuals.add(printSettings.print(actual));
+        }
+        this.actuals = Collections.unmodifiableList(actuals);
     }
 
     public String getWanted() {
         return wanted;
     }
 
-    public String getActual() {
-        return actual;
+    public List<String> getActuals() {
+        return actuals;
+    }
+
+    private static boolean isMultiLine(MatchableInvocation wanted, List<Invocation> allActualInvocations) {
+        boolean isWantedMultiline = wanted.toString().contains("\n");
+        boolean isAnyActualMultiline = false;
+        for (Invocation invocation : allActualInvocations) {
+            isAnyActualMultiline |= invocation.toString().contains("\n");
+        }
+        return isWantedMultiline || isAnyActualMultiline;
     }
 }
diff --git a/src/main/java/org/mockito/internal/verification/checkers/MissingInvocationChecker.java b/src/main/java/org/mockito/internal/verification/checkers/MissingInvocationChecker.java
index 3f89c77787..c13aa0098c 100644
--- a/src/main/java/org/mockito/internal/verification/checkers/MissingInvocationChecker.java
+++ b/src/main/java/org/mockito/internal/verification/checkers/MissingInvocationChecker.java
@@ -17,8 +17,10 @@
 import java.util.List;
 
 import org.mockito.internal.reporting.SmartPrinter;
+import org.mockito.internal.util.collections.ListUtil;
 import org.mockito.internal.verification.api.InOrderContext;
 import org.mockito.invocation.Invocation;
+import org.mockito.invocation.Location;
 import org.mockito.invocation.MatchableInvocation;
 
 public class MissingInvocationChecker {
@@ -39,8 +41,15 @@ public static void checkMissingInvocation(List<Invocation> invocations, Matchabl
         }
 
         Integer[] indexesOfSuspiciousArgs = getSuspiciouslyNotMatchingArgsIndexes(wanted.getMatchers(), similar.getArguments());
-        SmartPrinter smartPrinter = new SmartPrinter(wanted, similar, indexesOfSuspiciousArgs);
-        throw argumentsAreDifferent(smartPrinter.getWanted(), smartPrinter.getActual(), similar.getLocation());
+        SmartPrinter smartPrinter = new SmartPrinter(wanted, invocations, indexesOfSuspiciousArgs);
+        List<Location> actualLocations = ListUtil.convert(invocations, new ListUtil.Converter<Invocation, Location>() {
+            @Override
+            public Location convert(Invocation invocation) {
+                return invocation.getLocation();
+            }
+        });
+
+        throw argumentsAreDifferent(smartPrinter.getWanted(), smartPrinter.getActuals(), actualLocations);
 
     }
 


Test patch:
diff --git a/src/test/java/org/mockito/internal/verification/SmartPrinterTest.java b/src/test/java/org/mockito/internal/verification/SmartPrinterTest.java
index 4918991d3d..d786762a8d 100644
--- a/src/test/java/org/mockito/internal/verification/SmartPrinterTest.java
+++ b/src/test/java/org/mockito/internal/verification/SmartPrinterTest.java
@@ -36,7 +36,9 @@ public void shouldPrintBothInMultilinesWhenFirstIsMulti() {
 
         //then
         assertThat(printer.getWanted()).contains("\n");
-        assertThat(printer.getActual()).contains("\n");
+        for (String actual : printer.getActuals()) {
+            assertThat(actual).contains("\n");
+        }
     }
 
     @Test
@@ -46,7 +48,9 @@ public void shouldPrintBothInMultilinesWhenSecondIsMulti() {
 
         //then
         assertThat(printer.getWanted()).contains("\n");
-        assertThat(printer.getActual()).contains("\n");
+        for (String actual : printer.getActuals()) {
+            assertThat(actual).contains("\n");
+        }
     }
 
     @Test
@@ -56,7 +60,9 @@ public void shouldPrintBothInMultilinesWhenBothAreMulti() {
 
         //then
         assertThat(printer.getWanted()).contains("\n");
-        assertThat(printer.getActual()).contains("\n");
+        for (String actual : printer.getActuals()) {
+            assertThat(actual).contains("\n");
+        }
     }
 
     @Test
@@ -66,6 +72,8 @@ public void shouldPrintBothInSingleLineWhenBothAreShort() {
 
         //then
         assertThat(printer.getWanted()).doesNotContain("\n");
-        assertThat(printer.getActual()).doesNotContain("\n");
+        for (String actual : printer.getActuals()) {
+            assertThat(actual).doesNotContain("\n");
+        }
     }
 }
diff --git a/src/test/java/org/mockito/internal/verification/checkers/MissingInvocationCheckerTest.java b/src/test/java/org/mockito/internal/verification/checkers/MissingInvocationCheckerTest.java
index 107f5ce8f8..c71404ac3e 100644
--- a/src/test/java/org/mockito/internal/verification/checkers/MissingInvocationCheckerTest.java
+++ b/src/test/java/org/mockito/internal/verification/checkers/MissingInvocationCheckerTest.java
@@ -67,7 +67,7 @@ public void shouldReportWantedInvocationDiffersFromActual() {
 
         exception.expectMessage("Argument(s) are different! Wanted:");
         exception.expectMessage("mock.intArgumentMethod(2222);");
-        exception.expectMessage("Actual invocation has different arguments:");
+        exception.expectMessage("Actual invocations have different arguments:");
         exception.expectMessage("mock.intArgumentMethod(1111);");
 
         MissingInvocationChecker.checkMissingInvocation(invocations, wanted);
@@ -82,7 +82,7 @@ public void shouldReportUsingInvocationDescription() {
 
         exception.expectMessage("Argument(s) are different! Wanted:");
         exception.expectMessage("mock.intArgumentMethod(MyCoolPrint(2222));");
-        exception.expectMessage("Actual invocation has different arguments:");
+        exception.expectMessage("Actual invocations have different arguments:");
         exception.expectMessage("mock.intArgumentMethod(MyCoolPrint(1111));");
 
         MissingInvocationChecker.checkMissingInvocation(invocations, wanted);
@@ -103,7 +103,7 @@ private InvocationBuilder buildDifferentMethod() {
     static class CustomInvocationBuilder extends InvocationBuilder {
         @Override
         protected Invocation createInvocation(MockReference<Object> mockRef, MockitoMethod mockitoMethod, final Object[] arguments,
-                                              RealMethod realMethod, Location location, int sequenceNumber) {
+            RealMethod realMethod, Location location, int sequenceNumber) {
             return new InterceptedInvocation(mockRef, mockitoMethod, arguments, realMethod, location, sequenceNumber) {
                 @Override
                 public List<ArgumentMatcher> getArgumentsAsMatchers() {
diff --git a/src/test/java/org/mockito/internal/verification/checkers/MissingInvocationInOrderCheckerTest.java b/src/test/java/org/mockito/internal/verification/checkers/MissingInvocationInOrderCheckerTest.java
index 51cbc02c70..398e3b1b38 100644
--- a/src/test/java/org/mockito/internal/verification/checkers/MissingInvocationInOrderCheckerTest.java
+++ b/src/test/java/org/mockito/internal/verification/checkers/MissingInvocationInOrderCheckerTest.java
@@ -77,7 +77,7 @@ public void shouldReportArgumentsAreDifferent() throws Exception {
 
 		exception.expectMessage("Argument(s) are different! Wanted:");
 		exception.expectMessage("mock.intArgumentMethod(2222);");
-		exception.expectMessage("Actual invocation has different arguments:");
+		exception.expectMessage("Actual invocations have different arguments:");
 		exception.expectMessage("mock.intArgumentMethod(1111);");
 
     	checkMissingInvocation(invocations, wanted, context);
diff --git a/src/test/java/org/mockitousage/verification/DescriptiveMessagesOnVerificationInOrderErrorsTest.java b/src/test/java/org/mockitousage/verification/DescriptiveMessagesOnVerificationInOrderErrorsTest.java
index e920ad8f4e..ef7770542f 100644
--- a/src/test/java/org/mockitousage/verification/DescriptiveMessagesOnVerificationInOrderErrorsTest.java
+++ b/src/test/java/org/mockitousage/verification/DescriptiveMessagesOnVerificationInOrderErrorsTest.java
@@ -97,7 +97,7 @@ public void shouldPrintVerificationInOrderErrorAndShowWantedAndActual() {
             inOrder.verify(one).simpleMethod(999);
             fail();
         } catch (org.mockito.exceptions.verification.opentest4j.ArgumentsAreDifferent e) {
-            assertThat(e).hasMessageContaining("has different arguments");
+            assertThat(e).hasMessageContaining("have different arguments");
         }
     }
 
diff --git a/src/test/java/org/mockitousage/verification/DescriptiveMessagesWhenVerificationFailsTest.java b/src/test/java/org/mockitousage/verification/DescriptiveMessagesWhenVerificationFailsTest.java
index 729343471f..305e8ac112 100644
--- a/src/test/java/org/mockitousage/verification/DescriptiveMessagesWhenVerificationFailsTest.java
+++ b/src/test/java/org/mockitousage/verification/DescriptiveMessagesWhenVerificationFailsTest.java
@@ -82,7 +82,7 @@ public void should_print_actual_and_wanted_in_line() {
 
             String actual =
                     "\n" +
-                    "Actual invocation has different arguments:" +
+                    "Actual invocations have different arguments:" +
                     "\n" +
                     "iMethods.varargs(1, 2);";
 
@@ -116,7 +116,7 @@ public void should_print_actual_and_wanted_in_multiple_lines() {
 
             String actual =
                     "\n" +
-                    "Actual invocation has different arguments:" +
+                    "Actual invocations have different arguments:" +
                     "\n" +
                     "iMethods.varargs(" +
                     "\n" +
diff --git a/src/test/java/org/mockitousage/verification/VerifyPrintsAllInvocationsOnErrorTest.java b/src/test/java/org/mockitousage/verification/VerifyPrintsAllInvocationsOnErrorTest.java
new file mode 100644
index 0000000000..96f4c442b1
--- /dev/null
+++ b/src/test/java/org/mockitousage/verification/VerifyPrintsAllInvocationsOnErrorTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2007 Mockito contributors
+ * This program is made available under the terms of the MIT License.
+ */
+package org.mockitousage.verification;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.mockito.exceptions.verification.opentest4j.ArgumentsAreDifferent;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+
+
+public class VerifyPrintsAllInvocationsOnErrorTest {
+
+    @Test
+    public void shouldPrintAllInvocationsOnError() {
+        ExampleBuilder mockBuilder = Mockito.mock(ExampleBuilder.class);
+        mockBuilder.with("key1", "val1");
+        mockBuilder.with("key2", "val2");
+        try {
+            Mockito.verify(mockBuilder).with("key1", "wrongValue");
+            fail();
+        }
+        catch (ArgumentsAreDifferent e) {
+            assertThat(e).hasMessageContaining("exampleBuilder.with(\"key1\", \"val1\")");
+            assertThat(e).hasMessageContaining("exampleBuilder.with(\"key2\", \"val2\"");
+        }
+    }
+
+
+    private static class ExampleBuilder {
+        public ExampleBuilder with(String key, String val) {
+            return this;
+        }
+    }
+}