Repository: mockito/mockito
Issue: Make invocation to drive its printing

Right now the `toString()` of an `Invocation` is ignored when it's being printed for a verification error, this prevents mockito-scala from providing a custom representation of the arguments via the implicit context.

With a simple change to drive the printing from within the `Invocation` the problem can be solved

Issue comments:

Pull request review comments:
What is the reasoning for making this protected? It is an internal class, so it should not be subclassed.
I don't see these being used anywhere else. Moreover, they are not on the interface `Invocation`. If we were to add them, they have to be on the interface.
I currently need to extend it to replace the InvocationMatcher, so I made it protected just in case, but I'm happy to leave it private if you prefer so
I'm calling it from mockito-scala, should I add it to the interface then?
We cannot do it. It's leaking internal API via a public API.
>should I add it to the interface then?

Please no. MockReference is an internal type and we should not leak it from a public interface.
@TimvdLippe, I'm ok with changing private method to protected on an internal type so that mockito-scala integration is easier in the short term. @bbonanno is aware that we don't guarantee compatibility for internal API in the long term.
This is good! We can add this method to the public API because it returns a public type.
Can you add "since" tag because it's a new public API?
Can you add a note that the method converts each argument using ArgumentMatchers.eq() or AdditionalMatchers.arryEq() and that it is used internally for the purposes of human-readable invocation printing.

I don't want users to confuse this new method with the MatchableInvocation.getMatchers() method.
:)
Can you use Javadoc link? Thank you :)
Ups, sorry about that

Patch that resolved the issue:
diff --git a/src/main/java/org/mockito/internal/invocation/InterceptedInvocation.java b/src/main/java/org/mockito/internal/invocation/InterceptedInvocation.java
index 37cc451829..ec1fcba173 100644
--- a/src/main/java/org/mockito/internal/invocation/InterceptedInvocation.java
+++ b/src/main/java/org/mockito/internal/invocation/InterceptedInvocation.java
@@ -4,6 +4,7 @@
  */
 package org.mockito.internal.invocation;
 
+import org.mockito.ArgumentMatcher;
 import org.mockito.internal.invocation.mockref.MockReference;
 import org.mockito.internal.exceptions.VerificationAwareInvocation;
 import org.mockito.internal.reporting.PrintSettings;
@@ -13,8 +14,10 @@
 
 import java.lang.reflect.Method;
 import java.util.Arrays;
+import java.util.List;
 
 import static org.mockito.internal.exceptions.Reporter.cannotCallAbstractRealMethod;
+import static org.mockito.internal.invocation.ArgumentsProcessor.argumentsToMatchers;
 
 public class InterceptedInvocation implements Invocation, VerificationAwareInvocation {
 
@@ -119,6 +122,23 @@ public <T> T getArgument(int index) {
         return (T) arguments[index];
     }
 
+    public MockReference<Object> getMockRef() {
+        return mockRef;
+    }
+
+    public MockitoMethod getMockitoMethod() {
+        return mockitoMethod;
+    }
+
+    public RealMethod getRealMethod() {
+        return realMethod;
+    }
+
+    @Override
+    public List<ArgumentMatcher> getArgumentsAsMatchers() {
+        return argumentsToMatchers(getArguments());
+    }
+
     @Override
     public <T> T getArgument(int index, Class<T> clazz) {
         return clazz.cast(arguments[index]);
@@ -154,7 +174,7 @@ private boolean equalArguments(Object[] arguments) {
     }
 
     public String toString() {
-        return new PrintSettings().print(ArgumentsProcessor.argumentsToMatchers(getArguments()), this);
+        return new PrintSettings().print(getArgumentsAsMatchers(), this);
     }
 
     public final static RealMethod NO_OP = new RealMethod() {
diff --git a/src/main/java/org/mockito/internal/invocation/InvocationMatcher.java b/src/main/java/org/mockito/internal/invocation/InvocationMatcher.java
index 83a28ff9de..bc14300a3e 100644
--- a/src/main/java/org/mockito/internal/invocation/InvocationMatcher.java
+++ b/src/main/java/org/mockito/internal/invocation/InvocationMatcher.java
@@ -5,7 +5,6 @@
 
 package org.mockito.internal.invocation;
 
-import static org.mockito.internal.invocation.ArgumentsProcessor.argumentsToMatchers;
 import static org.mockito.internal.invocation.MatcherApplicationStrategy.getMatcherApplicationStrategyFor;
 import static org.mockito.internal.invocation.TypeSafeMatching.matchesTypeSafe;
 
@@ -36,7 +35,7 @@ public class InvocationMatcher implements MatchableInvocation, DescribedInvocati
     public InvocationMatcher(Invocation invocation, List<ArgumentMatcher> matchers) {
         this.invocation = invocation;
         if (matchers.isEmpty()) {
-            this.matchers = (List) argumentsToMatchers(invocation.getArguments());
+            this.matchers = (List) invocation.getArgumentsAsMatchers();
         } else {
             this.matchers = (List) matchers;
         }
diff --git a/src/main/java/org/mockito/internal/reporting/PrintSettings.java b/src/main/java/org/mockito/internal/reporting/PrintSettings.java
index 7986c35cec..4fc59926ea 100644
--- a/src/main/java/org/mockito/internal/reporting/PrintSettings.java
+++ b/src/main/java/org/mockito/internal/reporting/PrintSettings.java
@@ -5,7 +5,6 @@
 package org.mockito.internal.reporting;
 
 import org.mockito.ArgumentMatcher;
-import org.mockito.internal.invocation.ArgumentsProcessor;
 import org.mockito.internal.matchers.text.MatchersPrinter;
 import org.mockito.internal.util.MockUtil;
 import org.mockito.invocation.Invocation;
@@ -55,7 +54,7 @@ public String print(List<ArgumentMatcher> matchers, Invocation invocation) {
     }
 
     public String print(Invocation invocation) {
-        return print(ArgumentsProcessor.argumentsToMatchers(invocation.getArguments()), invocation);
+        return print(invocation.getArgumentsAsMatchers(), invocation);
     }
 
     public String print(MatchableInvocation invocation) {
diff --git a/src/main/java/org/mockito/invocation/Invocation.java b/src/main/java/org/mockito/invocation/Invocation.java
index 131db8441f..fa2e324114 100644
--- a/src/main/java/org/mockito/invocation/Invocation.java
+++ b/src/main/java/org/mockito/invocation/Invocation.java
@@ -4,8 +4,11 @@
  */
 package org.mockito.invocation;
 
+import org.mockito.ArgumentMatcher;
 import org.mockito.NotExtensible;
 
+import java.util.List;
+
 /**
  * A method call on a mock object. Contains all information and state needed for the Mockito framework to operate.
  * This API might be useful for developers who extend Mockito.
@@ -47,6 +50,16 @@ public interface Invocation extends InvocationOnMock, DescribedInvocation {
      */
     Object[] getRawArguments();
 
+    /**
+     * Wraps each argument using {@link org.mockito.ArgumentMatchers#eq(Object)} or
+     * {@link org.mockito.AdditionalMatchers#aryEq(Object[])}
+     * Used internally for the purposes of human-readable invocation printing.
+     *
+     * @return a list of {@link ArgumentMatcher} wrapping each of this invocation arguments
+     * @since 2.25.6
+     */
+    List<ArgumentMatcher> getArgumentsAsMatchers();
+
     /**
      * Returns unprocessed arguments whereas {@link #getArguments()} returns
      * arguments already processed (e.g. varargs expended, etc.).


Test patch:
diff --git a/src/test/java/org/mockito/internal/invocation/InvocationBuilder.java b/src/test/java/org/mockito/internal/invocation/InvocationBuilder.java
index 4fc5cb1c1c..4e01535027 100644
--- a/src/test/java/org/mockito/internal/invocation/InvocationBuilder.java
+++ b/src/test/java/org/mockito/internal/invocation/InvocationBuilder.java
@@ -6,6 +6,7 @@
 package org.mockito.internal.invocation;
 
 import org.mockito.Mockito;
+import org.mockito.internal.invocation.mockref.MockReference;
 import org.mockito.internal.invocation.mockref.MockStrongReference;
 import org.mockito.internal.debugging.LocationImpl;
 import org.mockito.invocation.Invocation;
@@ -61,7 +62,7 @@ public Invocation toInvocation() {
             }
         }
 
-        Invocation i = new InterceptedInvocation(new MockStrongReference<Object>(mock, false),
+        Invocation i = createInvocation(new MockStrongReference<Object>(mock, false),
             new SerializableMethod(method),
             args,
             NO_OP,
@@ -73,6 +74,11 @@ public Invocation toInvocation() {
         return i;
     }
 
+    protected Invocation createInvocation(MockReference<Object> mockRef, MockitoMethod mockitoMethod, Object[] arguments,
+                                          RealMethod realMethod, Location location, int sequenceNumber) {
+        return new InterceptedInvocation(mockRef, mockitoMethod, arguments, realMethod, location, sequenceNumber);
+    }
+
     public InvocationBuilder method(String methodName) {
         this.methodName = methodName;
         return this;
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 15a57b96d6..107f5ce8f8 100644
--- a/src/test/java/org/mockito/internal/verification/checkers/MissingInvocationCheckerTest.java
+++ b/src/test/java/org/mockito/internal/verification/checkers/MissingInvocationCheckerTest.java
@@ -5,78 +5,125 @@
 
 package org.mockito.internal.verification.checkers;
 
-import static java.util.Arrays.asList;
-
-import java.util.List;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
+import org.mockito.ArgumentMatcher;
 import org.mockito.Mock;
-import org.mockito.exceptions.verification.WantedButNotInvoked;
 import org.mockito.exceptions.verification.opentest4j.ArgumentsAreDifferent;
-import org.mockito.internal.invocation.InvocationBuilder;
-import org.mockito.internal.invocation.InvocationMatcher;
+import org.mockito.exceptions.verification.WantedButNotInvoked;
+import org.mockito.internal.invocation.*;
+import org.mockito.internal.invocation.mockref.MockReference;
 import org.mockito.invocation.Invocation;
+import org.mockito.invocation.Location;
 import org.mockitousage.IMethods;
 import org.mockitoutil.TestBase;
 
-public class MissingInvocationCheckerTest extends TestBase {
-
-	private InvocationMatcher wanted;
-	private List<Invocation> invocations;
-
-	@Mock
-	private IMethods mock;
-
-	@Rule
-	public ExpectedException exception = ExpectedException.none();
-
-	@Test
-	public void shouldPassBecauseActualInvocationFound() {
-		wanted = buildSimpleMethod().toInvocationMatcher();
-		invocations = asList(buildSimpleMethod().toInvocation());
-
-		MissingInvocationChecker.checkMissingInvocation(invocations, wanted);
-	}
-
-	@Test
-	public void shouldReportWantedButNotInvoked() {
-		wanted = buildSimpleMethod().toInvocationMatcher();
-		invocations = asList(buildDifferentMethod().toInvocation());
-
-		exception.expect(WantedButNotInvoked.class);
-		exception.expectMessage("Wanted but not invoked:");
-		exception.expectMessage("mock.simpleMethod()");
-		exception.expectMessage("However, there was exactly 1 interaction with this mock:");
-		exception.expectMessage("mock.differentMethod();");
-
-		MissingInvocationChecker.checkMissingInvocation(invocations, wanted);
-	}
-
-	@Test
-	public void shouldReportWantedInvocationDiffersFromActual() {
-		wanted = buildIntArgMethod().arg(2222).toInvocationMatcher();
-		invocations = asList(buildIntArgMethod().arg(1111).toInvocation());
-
-		exception.expect(ArgumentsAreDifferent.class);
-
-		exception.expectMessage("Argument(s) are different! Wanted:");
-		exception.expectMessage("mock.intArgumentMethod(2222);");
-		exception.expectMessage("Actual invocation has different arguments:");
-		exception.expectMessage("mock.intArgumentMethod(1111);");
-
-		MissingInvocationChecker.checkMissingInvocation(invocations, wanted);
-	}
+import java.util.ArrayList;
+import java.util.List;
 
-	private InvocationBuilder buildIntArgMethod() {
-		return new InvocationBuilder().mock(mock).method("intArgumentMethod").argTypes(int.class);
-	}
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
 
-	private InvocationBuilder buildSimpleMethod() {
-		return new InvocationBuilder().mock(mock).simpleMethod();
-	}
+public class MissingInvocationCheckerTest extends TestBase {
 
-	private InvocationBuilder buildDifferentMethod() {
-		return new InvocationBuilder().mock(mock).differentMethod();
-	}
+    private InvocationMatcher wanted;
+    private List<Invocation> invocations;
+
+    @Mock
+    private IMethods mock;
+
+    @Rule
+    public ExpectedException exception = ExpectedException.none();
+
+    @Test
+    public void shouldPassBecauseActualInvocationFound() {
+        wanted = buildSimpleMethod().toInvocationMatcher();
+        invocations = asList(buildSimpleMethod().toInvocation());
+
+        MissingInvocationChecker.checkMissingInvocation(invocations, wanted);
+    }
+
+    @Test
+    public void shouldReportWantedButNotInvoked() {
+        wanted = buildSimpleMethod().toInvocationMatcher();
+        invocations = asList(buildDifferentMethod().toInvocation());
+
+        exception.expect(WantedButNotInvoked.class);
+        exception.expectMessage("Wanted but not invoked:");
+        exception.expectMessage("mock.simpleMethod()");
+        exception.expectMessage("However, there was exactly 1 interaction with this mock:");
+        exception.expectMessage("mock.differentMethod();");
+
+        MissingInvocationChecker.checkMissingInvocation(invocations, wanted);
+    }
+
+    @Test
+    public void shouldReportWantedInvocationDiffersFromActual() {
+        wanted = buildIntArgMethod(new InvocationBuilder()).arg(2222).toInvocationMatcher();
+        invocations = asList(buildIntArgMethod(new InvocationBuilder()).arg(1111).toInvocation());
+
+        exception.expect(ArgumentsAreDifferent.class);
+
+        exception.expectMessage("Argument(s) are different! Wanted:");
+        exception.expectMessage("mock.intArgumentMethod(2222);");
+        exception.expectMessage("Actual invocation has different arguments:");
+        exception.expectMessage("mock.intArgumentMethod(1111);");
+
+        MissingInvocationChecker.checkMissingInvocation(invocations, wanted);
+    }
+
+    @Test
+    public void shouldReportUsingInvocationDescription() {
+        wanted = buildIntArgMethod(new CustomInvocationBuilder()).arg(2222).toInvocationMatcher();
+        invocations = singletonList(buildIntArgMethod(new CustomInvocationBuilder()).arg(1111).toInvocation());
+
+        exception.expect(ArgumentsAreDifferent.class);
+
+        exception.expectMessage("Argument(s) are different! Wanted:");
+        exception.expectMessage("mock.intArgumentMethod(MyCoolPrint(2222));");
+        exception.expectMessage("Actual invocation has different arguments:");
+        exception.expectMessage("mock.intArgumentMethod(MyCoolPrint(1111));");
+
+        MissingInvocationChecker.checkMissingInvocation(invocations, wanted);
+    }
+
+    private InvocationBuilder buildIntArgMethod(InvocationBuilder invocationBuilder) {
+        return invocationBuilder.mock(mock).method("intArgumentMethod").argTypes(int.class);
+    }
+
+    private InvocationBuilder buildSimpleMethod() {
+        return new InvocationBuilder().mock(mock).simpleMethod();
+    }
+
+    private InvocationBuilder buildDifferentMethod() {
+        return new InvocationBuilder().mock(mock).differentMethod();
+    }
+
+    static class CustomInvocationBuilder extends InvocationBuilder {
+        @Override
+        protected Invocation createInvocation(MockReference<Object> mockRef, MockitoMethod mockitoMethod, final Object[] arguments,
+                                              RealMethod realMethod, Location location, int sequenceNumber) {
+            return new InterceptedInvocation(mockRef, mockitoMethod, arguments, realMethod, location, sequenceNumber) {
+                @Override
+                public List<ArgumentMatcher> getArgumentsAsMatchers() {
+                    List<ArgumentMatcher> matchers = new ArrayList<ArgumentMatcher>();
+                    for (final Object argument : getRawArguments()) {
+                        matchers.add(new ArgumentMatcher() {
+                            @Override
+                            public boolean matches(Object a) {
+                                return a == argument;
+                            }
+
+                            @Override
+                            public String toString() {
+                                return "MyCoolPrint(" + argument + ")";
+                            }
+                        });
+                    }
+                    return matchers;
+                }
+            };
+        }
+    }
 }