From 483e15f9f940716a85f2d58f6c28981136790c4e Mon Sep 17 00:00:00 2001 From: Andy Coates <8012398+big-andy-coates@users.noreply.github.com> Date: Thu, 22 Dec 2022 16:04:55 +0000 Subject: [PATCH] Add `type()` method to `ArgumentMatcher` (#2807) Using the new `type()`, we can differentiate between matching all varargs or only one argument of the varargs. # Benefits: Because this approach leaves `VarargsMatcher` untouched, it does not require additional existing matchers to implement `VarargsMatcher` to fix issues such as https://github.com/mockito/mockito/issues/567. Where as the first PR would require `Null` and `NotNull` to be marked `VarargsMatcher`. This PR creates new variants of `isNotNull` and `isNull` to address https://github.com/mockito/mockito/issues/567. Having `InstanceOf` override `type()` provides a workable solution to https://github.com/mockito/mockito/issues/1593. Having `equals` override `type` addresses https://github.com/mockito/mockito/issues/1222. # Downsides The obvious downside is that this changes the public `ArgumentMatcher` interface, though in a backwards compatible way. ## Known limitation The main limitation I'm aware of, is not a new limitation. It is that it is not possible to assert only a single parameter is passed to the vararg parameter, when using a `VarargMatcher`, e.g. `any()`. (ref: https://github.com/mockito/mockito/issues/1593). For example: ```java // Given method: int vararg(String... args); // I want to mock this invocation: mock.vararag("one param"); // ...but not these: mock.vararg(); mock.vararg("more than", "one param"); ``` There is no current way to do this. This is because in the following intuitive mocking: ```java given(mock.vararg(any(String.class))).willReturn(1); ``` ... matches zero or more vararg parameters, as the `any()` method is using `VarargMatcher`. It seems to me that `VarargMatcher` is... a little broken! This is maybe something that should be consider a candiate for fixing in the next major version bump. While it is not possible to fix any `VarargMatcher` based matchers in a backwards compatible way, this the approach in this PR it is possible to mock/verify exactly one vararg param using `isA`, rather than `any`: ```java @Test public void shouldMatchExactlyOnParam() { mock.varargs("one param"); verify(mock).varargs(isA(String.class)); } @Test public void shouldNotMatchMoreParams() { mock.varargs("two", "params"); verify(mock, never()).varargs(isA(String.class)); } @Test public void shouldMatchAnyNumberOfParams() { mock.varargs("two", "params"); verify(mock).varargs(isA(String[].class)); } ``` ... because `isA` does not implement `VarargsMatcher`, and so can work as expected once it implements `type()`. Fixes #2796 Fixes #567 Fixes #584 Fixes #1222 Fixes #1498 --- src/main/java/org/mockito/ArgumentCaptor.java | 3 +- .../java/org/mockito/ArgumentMatcher.java | 41 +++ .../java/org/mockito/ArgumentMatchers.java | 57 +++ .../hamcrest/HamcrestArgumentMatcher.java | 1 + .../MatcherApplicationStrategy.java | 36 +- .../org/mockito/internal/matchers/And.java | 7 + .../org/mockito/internal/matchers/Any.java | 5 + .../internal/matchers/CapturingMatcher.java | 11 + .../org/mockito/internal/matchers/Equals.java | 5 + .../mockito/internal/matchers/InstanceOf.java | 12 +- .../org/mockito/internal/matchers/Not.java | 5 + .../mockito/internal/matchers/NotNull.java | 16 +- .../org/mockito/internal/matchers/Null.java | 15 +- .../org/mockito/internal/matchers/Or.java | 7 + .../org/mockito/internal/matchers/Same.java | 5 + .../internal/matchers/VarargMatcher.java | 9 + .../invocation/InvocationMatcherTest.java | 4 +- .../MatcherApplicationStrategyTest.java | 16 + .../mockito/internal/matchers/AndTest.java | 62 ++++ .../matchers/CapturingMatcherTest.java | 8 +- .../mockito/internal/matchers/EqualsTest.java | 13 + .../org/mockito/internal/matchers/OrTest.java | 62 ++++ .../mockito/internal/matchers/SameTest.java | 25 ++ src/test/java/org/mockitousage/IMethods.java | 12 +- .../java/org/mockitousage/MethodsImpl.java | 23 +- .../mockitousage/matchers/VarargsTest.java | 330 ++++++++++++++++-- 26 files changed, 723 insertions(+), 67 deletions(-) create mode 100644 src/test/java/org/mockito/internal/matchers/AndTest.java create mode 100644 src/test/java/org/mockito/internal/matchers/OrTest.java create mode 100644 src/test/java/org/mockito/internal/matchers/SameTest.java diff --git a/src/main/java/org/mockito/ArgumentCaptor.java b/src/main/java/org/mockito/ArgumentCaptor.java index afb3add2ba..ed9e398f91 100644 --- a/src/main/java/org/mockito/ArgumentCaptor.java +++ b/src/main/java/org/mockito/ArgumentCaptor.java @@ -62,11 +62,12 @@ @CheckReturnValue public class ArgumentCaptor { - private final CapturingMatcher capturingMatcher = new CapturingMatcher(); + private final CapturingMatcher capturingMatcher; private final Class clazz; private ArgumentCaptor(Class clazz) { this.clazz = clazz; + this.capturingMatcher = new CapturingMatcher(clazz); } /** diff --git a/src/main/java/org/mockito/ArgumentMatcher.java b/src/main/java/org/mockito/ArgumentMatcher.java index d0324b6f3f..ad52e1ecae 100644 --- a/src/main/java/org/mockito/ArgumentMatcher.java +++ b/src/main/java/org/mockito/ArgumentMatcher.java @@ -110,6 +110,7 @@ * @param type of argument * @since 2.1.0 */ +@FunctionalInterface public interface ArgumentMatcher { /** @@ -125,4 +126,44 @@ public interface ArgumentMatcher { * @return true if this matcher accepts the given argument. */ boolean matches(T argument); + + /** + * The type of the argument this matcher matches. + * + *

This method is used to differentiate between a matcher used to match a raw vararg array parameter + * from a matcher used to match a single value passed as a vararg parameter. + * + *

Where the matcher: + *

    + *
  • is at the parameter index of a vararg parameter
  • + *
  • is the last matcher passed
  • + *
  • this method returns a type assignable to the vararg parameter's raw type, i.e. its array type.
  • + *
+ * + * ...then the matcher is matched against the raw vararg parameter, rather than the first element of the raw parameter. + * + *

For example: + * + *


+     *  // Given vararg method with signature:
+     *  int someVarargMethod(String... args);
+     *
+     *  // The following will match invocations with any number of parameters, i.e. any number of elements in the raw array.
+     *  mock.someVarargMethod(isA(String[].class));
+     *
+     *  // The following will match invocations with a single parameter, i.e. one string in the raw array.
+     *  mock.someVarargMethod(isA(String.class));
+     *
+     *  // The following will match invocations with two parameters, i.e. two strings in the raw array
+     *  mock.someVarargMethod(isA(String.class), isA(String.class));
+     * 
+ * + *

Only matcher implementations that can conceptually match a raw vararg parameter should override this method. + * + * @return the type this matcher handles. The default value of {@link Void} means the type is not known. + * @since 5.0.0 + */ + default Class type() { + return Void.class; + } } diff --git a/src/main/java/org/mockito/ArgumentMatchers.java b/src/main/java/org/mockito/ArgumentMatchers.java index d969bc79e7..28986e1648 100644 --- a/src/main/java/org/mockito/ArgumentMatchers.java +++ b/src/main/java/org/mockito/ArgumentMatchers.java @@ -699,6 +699,23 @@ public static T isNull() { return null; } + /** + * null argument. + * + *

+ * See examples in javadoc for {@link ArgumentMatchers} class + *

+ * + * @param type the type of the argument being matched. + * @return null. + * @see #isNotNull(Class) + * @since 5.0.0 + */ + public static T isNull(Class type) { + reportMatcher(new Null<>(type)); + return null; + } + /** * Not null argument. * @@ -717,6 +734,26 @@ public static T notNull() { return null; } + /** + * Not null argument. + * + *

+ * Alias to {@link ArgumentMatchers#isNotNull()} + *

+ * + *

+ * See examples in javadoc for {@link ArgumentMatchers} class + *

+ * + * @param type the type of the argument being matched. + * @return null. + * @since 5.0.0 + */ + public static T notNull(Class type) { + reportMatcher(new NotNull<>(type)); + return null; + } + /** * Not null argument. * @@ -735,6 +772,26 @@ public static T isNotNull() { return notNull(); } + /** + * Not null argument. + * + *

+ * Alias to {@link ArgumentMatchers#notNull(Class)} + *

+ * + *

+ * See examples in javadoc for {@link ArgumentMatchers} class + *

+ * + * @param type the type of the argument being matched. + * @return null. + * @see #isNull() + * @since 5.0.0 + */ + public static T isNotNull(Class type) { + return notNull(type); + } + /** * Argument that is either null or of the given type. * diff --git a/src/main/java/org/mockito/internal/hamcrest/HamcrestArgumentMatcher.java b/src/main/java/org/mockito/internal/hamcrest/HamcrestArgumentMatcher.java index 99869faf73..db526546eb 100644 --- a/src/main/java/org/mockito/internal/hamcrest/HamcrestArgumentMatcher.java +++ b/src/main/java/org/mockito/internal/hamcrest/HamcrestArgumentMatcher.java @@ -22,6 +22,7 @@ public boolean matches(Object argument) { return this.matcher.matches(argument); } + @SuppressWarnings("deprecation") public boolean isVarargMatcher() { return matcher instanceof VarargMatcher; } diff --git a/src/main/java/org/mockito/internal/invocation/MatcherApplicationStrategy.java b/src/main/java/org/mockito/internal/invocation/MatcherApplicationStrategy.java index 5e16f27040..f3c0111c64 100644 --- a/src/main/java/org/mockito/internal/invocation/MatcherApplicationStrategy.java +++ b/src/main/java/org/mockito/internal/invocation/MatcherApplicationStrategy.java @@ -58,17 +58,24 @@ public static MatcherApplicationStrategy getMatcherApplicationStrategyFor( * */ public boolean forEachMatcherAndArgument(ArgumentMatcherAction action) { + final boolean maybeVararg = + invocation.getMethod().isVarArgs() + && invocation.getRawArguments().length == matchers.size(); + + if (maybeVararg) { + final Class matcherType = lastMatcher().type(); + final Class paramType = lastParameterType(); + if (paramType.isAssignableFrom(matcherType)) { + return argsMatch(invocation.getRawArguments(), matchers, action); + } + } + if (invocation.getArguments().length == matchers.size()) { return argsMatch(invocation.getArguments(), matchers, action); } - final boolean isVararg = - invocation.getMethod().isVarArgs() - && invocation.getRawArguments().length == matchers.size() - && isLastMatcherVarargMatcher(matchers); - - if (isVararg) { - int times = varargLength(invocation); + if (maybeVararg && isLastMatcherVarargMatcher()) { + int times = varargLength(); final List> matchers = appendLastMatcherNTimes(times); return argsMatch(invocation.getArguments(), matchers, action); } @@ -91,8 +98,8 @@ private boolean argsMatch( return true; } - private static boolean isLastMatcherVarargMatcher(List> matchers) { - ArgumentMatcher argumentMatcher = lastMatcher(matchers); + private boolean isLastMatcherVarargMatcher() { + ArgumentMatcher argumentMatcher = lastMatcher(); if (argumentMatcher instanceof HamcrestArgumentMatcher) { return ((HamcrestArgumentMatcher) argumentMatcher).isVarargMatcher(); } @@ -101,7 +108,7 @@ private static boolean isLastMatcherVarargMatcher(List> appendLastMatcherNTimes( int timesToAppendLastMatcher) { - ArgumentMatcher lastMatcher = lastMatcher(matchers); + ArgumentMatcher lastMatcher = lastMatcher(); List> expandedMatchers = new ArrayList>(matchers); for (int i = 0; i < timesToAppendLastMatcher; i++) { @@ -110,13 +117,18 @@ private List> appendLastMatcherNTimes( return expandedMatchers; } - private static int varargLength(Invocation invocation) { + private int varargLength() { int rawArgumentCount = invocation.getRawArguments().length; int expandedArgumentCount = invocation.getArguments().length; return expandedArgumentCount - rawArgumentCount; } - private static ArgumentMatcher lastMatcher(List> matchers) { + private ArgumentMatcher lastMatcher() { return matchers.get(matchers.size() - 1); } + + private Class lastParameterType() { + final Class[] parameterTypes = invocation.getMethod().getParameterTypes(); + return parameterTypes[parameterTypes.length - 1]; + } } diff --git a/src/main/java/org/mockito/internal/matchers/And.java b/src/main/java/org/mockito/internal/matchers/And.java index 417e88dbbe..cffed323f1 100644 --- a/src/main/java/org/mockito/internal/matchers/And.java +++ b/src/main/java/org/mockito/internal/matchers/And.java @@ -23,6 +23,13 @@ public boolean matches(Object actual) { return m1.matches(actual) && m2.matches(actual); } + @Override + public Class type() { + return m1.type().isAssignableFrom(m2.type()) + ? m1.type() + : m2.type().isAssignableFrom(m1.type()) ? m2.type() : ArgumentMatcher.super.type(); + } + @Override public String toString() { return "and(" + m1 + ", " + m2 + ")"; diff --git a/src/main/java/org/mockito/internal/matchers/Any.java b/src/main/java/org/mockito/internal/matchers/Any.java index 7ad113feed..1a71a7e2bc 100644 --- a/src/main/java/org/mockito/internal/matchers/Any.java +++ b/src/main/java/org/mockito/internal/matchers/Any.java @@ -21,4 +21,9 @@ public boolean matches(Object actual) { public String toString() { return ""; } + + @Override + public Class type() { + return Object.class; + } } diff --git a/src/main/java/org/mockito/internal/matchers/CapturingMatcher.java b/src/main/java/org/mockito/internal/matchers/CapturingMatcher.java index 5138839ddb..84da8f57d9 100644 --- a/src/main/java/org/mockito/internal/matchers/CapturingMatcher.java +++ b/src/main/java/org/mockito/internal/matchers/CapturingMatcher.java @@ -9,6 +9,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -19,12 +20,17 @@ public class CapturingMatcher implements ArgumentMatcher, CapturesArguments, VarargMatcher, Serializable { + private final Class clazz; private final List arguments = new ArrayList<>(); private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock readLock = lock.readLock(); private final Lock writeLock = lock.writeLock(); + public CapturingMatcher(final Class clazz) { + this.clazz = Objects.requireNonNull(clazz); + } + @Override public boolean matches(Object argument) { return true; @@ -66,4 +72,9 @@ public void captureFrom(Object argument) { writeLock.unlock(); } } + + @Override + public Class type() { + return clazz; + } } diff --git a/src/main/java/org/mockito/internal/matchers/Equals.java b/src/main/java/org/mockito/internal/matchers/Equals.java index 8b07b2da75..bbfad627c4 100644 --- a/src/main/java/org/mockito/internal/matchers/Equals.java +++ b/src/main/java/org/mockito/internal/matchers/Equals.java @@ -22,6 +22,11 @@ public boolean matches(Object actual) { return Equality.areEqual(this.wanted, actual); } + @Override + public Class type() { + return wanted != null ? wanted.getClass() : ArgumentMatcher.super.type(); + } + @Override public String toString() { return describe(wanted); diff --git a/src/main/java/org/mockito/internal/matchers/InstanceOf.java b/src/main/java/org/mockito/internal/matchers/InstanceOf.java index 1b4b30e875..9ea762bb96 100644 --- a/src/main/java/org/mockito/internal/matchers/InstanceOf.java +++ b/src/main/java/org/mockito/internal/matchers/InstanceOf.java @@ -11,7 +11,7 @@ public class InstanceOf implements ArgumentMatcher, Serializable { - private final Class clazz; + final Class clazz; private final String description; public InstanceOf(Class clazz) { @@ -30,6 +30,11 @@ public boolean matches(Object actual) { || clazz.isAssignableFrom(actual.getClass())); } + @Override + public Class type() { + return clazz; + } + @Override public String toString() { return description; @@ -44,5 +49,10 @@ public VarArgAware(Class clazz) { public VarArgAware(Class clazz, String describedAs) { super(clazz, describedAs); } + + @Override + public Class type() { + return clazz; + } } } diff --git a/src/main/java/org/mockito/internal/matchers/Not.java b/src/main/java/org/mockito/internal/matchers/Not.java index bd35eafbad..62b91b5c64 100644 --- a/src/main/java/org/mockito/internal/matchers/Not.java +++ b/src/main/java/org/mockito/internal/matchers/Not.java @@ -22,6 +22,11 @@ public boolean matches(Object actual) { return !matcher.matches(actual); } + @Override + public Class type() { + return matcher.type(); + } + @Override public String toString() { return "not(" + matcher + ")"; diff --git a/src/main/java/org/mockito/internal/matchers/NotNull.java b/src/main/java/org/mockito/internal/matchers/NotNull.java index 2902f57061..f3f7fe7d05 100644 --- a/src/main/java/org/mockito/internal/matchers/NotNull.java +++ b/src/main/java/org/mockito/internal/matchers/NotNull.java @@ -5,20 +5,30 @@ package org.mockito.internal.matchers; import java.io.Serializable; +import java.util.Objects; import org.mockito.ArgumentMatcher; -public class NotNull implements ArgumentMatcher, Serializable { +public class NotNull implements ArgumentMatcher, Serializable { - public static final NotNull NOT_NULL = new NotNull(); + public static final NotNull NOT_NULL = new NotNull<>(Object.class); - private NotNull() {} + private final Class type; + + public NotNull(Class type) { + this.type = Objects.requireNonNull(type); + } @Override public boolean matches(Object actual) { return actual != null; } + @Override + public Class type() { + return type; + } + @Override public String toString() { return "notNull()"; diff --git a/src/main/java/org/mockito/internal/matchers/Null.java b/src/main/java/org/mockito/internal/matchers/Null.java index 69eee484a1..400170a5cc 100644 --- a/src/main/java/org/mockito/internal/matchers/Null.java +++ b/src/main/java/org/mockito/internal/matchers/Null.java @@ -5,20 +5,29 @@ package org.mockito.internal.matchers; import java.io.Serializable; +import java.util.Objects; import org.mockito.ArgumentMatcher; -public class Null implements ArgumentMatcher, Serializable { +public class Null implements ArgumentMatcher, Serializable { - public static final Null NULL = new Null(); + public static final Null NULL = new Null<>(Object.class); + private final Class type; - private Null() {} + public Null(Class type) { + this.type = Objects.requireNonNull(type); + } @Override public boolean matches(Object actual) { return actual == null; } + @Override + public Class type() { + return type; + } + @Override public String toString() { return "isNull()"; diff --git a/src/main/java/org/mockito/internal/matchers/Or.java b/src/main/java/org/mockito/internal/matchers/Or.java index ed7bbdeb4d..7354814323 100644 --- a/src/main/java/org/mockito/internal/matchers/Or.java +++ b/src/main/java/org/mockito/internal/matchers/Or.java @@ -23,6 +23,13 @@ public boolean matches(Object actual) { return m1.matches(actual) || m2.matches(actual); } + @Override + public Class type() { + return m1.type().isAssignableFrom(m2.type()) + ? m1.type() + : m2.type().isAssignableFrom(m1.type()) ? m2.type() : ArgumentMatcher.super.type(); + } + @Override public String toString() { return "or(" + m1 + ", " + m2 + ")"; diff --git a/src/main/java/org/mockito/internal/matchers/Same.java b/src/main/java/org/mockito/internal/matchers/Same.java index 0e23c5cd46..fa116b53f0 100644 --- a/src/main/java/org/mockito/internal/matchers/Same.java +++ b/src/main/java/org/mockito/internal/matchers/Same.java @@ -22,6 +22,11 @@ public boolean matches(Object actual) { return wanted == actual; } + @Override + public Class type() { + return wanted != null ? wanted.getClass() : ArgumentMatcher.super.type(); + } + @Override public String toString() { return "same(" + ValuePrinter.print(wanted) + ")"; diff --git a/src/main/java/org/mockito/internal/matchers/VarargMatcher.java b/src/main/java/org/mockito/internal/matchers/VarargMatcher.java index 43a27596f8..2dfd9b7549 100644 --- a/src/main/java/org/mockito/internal/matchers/VarargMatcher.java +++ b/src/main/java/org/mockito/internal/matchers/VarargMatcher.java @@ -9,5 +9,14 @@ /** * Internal interface that informs Mockito that the matcher is intended to capture varargs. * This information is needed when mockito collects the arguments. + * + * @deprecated use of this interface is deprecated as the behaviour it promotes has limitations. + * It is not recommended for new implementations to implement this method. + * + *

Instead, matchers should implement the {@link org.mockito.ArgumentMatcher#type()} method. + * If this method returns the same raw type as a vararg parameter, then Mockito will treat the + * matcher as matching the entire vararg array parameter, otherwise it will be treated as matching a single element. + * For an example, see {@link org.mockito.ArgumentMatchers#isNull(Class)}. */ +@Deprecated public interface VarargMatcher extends Serializable {} diff --git a/src/test/java/org/mockito/internal/invocation/InvocationMatcherTest.java b/src/test/java/org/mockito/internal/invocation/InvocationMatcherTest.java index 0b3d09d5f1..ab53be545f 100644 --- a/src/test/java/org/mockito/internal/invocation/InvocationMatcherTest.java +++ b/src/test/java/org/mockito/internal/invocation/InvocationMatcherTest.java @@ -136,7 +136,7 @@ public void should_be_similar_if_is_overloaded_but_used_with_different_arg() thr public void should_capture_arguments_from_invocation() throws Exception { // given Invocation invocation = new InvocationBuilder().args("1", 100).toInvocation(); - CapturingMatcher capturingMatcher = new CapturingMatcher(); + CapturingMatcher capturingMatcher = new CapturingMatcher(List.class); InvocationMatcher invocationMatcher = new InvocationMatcher(invocation, (List) asList(new Equals("1"), capturingMatcher)); @@ -167,7 +167,7 @@ public void should_capture_varargs_as_vararg() throws Exception { // given mock.mixedVarargs(1, "a", "b"); Invocation invocation = getLastInvocation(); - CapturingMatcher m = new CapturingMatcher(); + CapturingMatcher m = new CapturingMatcher(List.class); InvocationMatcher invocationMatcher = new InvocationMatcher(invocation, Arrays.asList(new Equals(1), m)); diff --git a/src/test/java/org/mockito/internal/invocation/MatcherApplicationStrategyTest.java b/src/test/java/org/mockito/internal/invocation/MatcherApplicationStrategyTest.java index baf7a5ad5a..ddd4333905 100644 --- a/src/test/java/org/mockito/internal/invocation/MatcherApplicationStrategyTest.java +++ b/src/test/java/org/mockito/internal/invocation/MatcherApplicationStrategyTest.java @@ -9,6 +9,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.internal.invocation.MatcherApplicationStrategy.getMatcherApplicationStrategyFor; import static org.mockito.internal.matchers.Any.ANY; @@ -225,6 +226,21 @@ public void shouldMatchAnyEvenIfMatcherIsWrappedInHamcrestMatcher() { recordAction.assertContainsExactly(argumentMatcher, argumentMatcher); } + @Test + public void shouldMatchAnyThatMatchesRawVarArgType() { + // given + invocation = varargs("1", "2"); + InstanceOf.VarArgAware any = new InstanceOf.VarArgAware(String[].class, ""); + matchers = asList(any); + + // when + getMatcherApplicationStrategyFor(invocation, matchers) + .forEachMatcherAndArgument(recordAction); + + // then + recordAction.assertContainsExactly(any); + } + private static class IntMatcher extends BaseMatcher implements VarargMatcher { public boolean matches(Object o) { return true; diff --git a/src/test/java/org/mockito/internal/matchers/AndTest.java b/src/test/java/org/mockito/internal/matchers/AndTest.java new file mode 100644 index 0000000000..3115576d9e --- /dev/null +++ b/src/test/java/org/mockito/internal/matchers/AndTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.matchers; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockitoutil.TestBase; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +public class AndTest extends TestBase { + + @Mock private ArgumentMatcher m1; + @Mock private ArgumentMatcher m2; + private And and; + + @Before + public void setUp() throws Exception { + and = new And(m1, m2); + } + + @Test + public void shouldReturnMatchingTypes() { + given(m1.type()).will(inv -> String.class); + given(m2.type()).will(inv -> String.class); + + assertThat(and.type()).isEqualTo(String.class); + } + + @Test + public void shouldDefaultMismatchingTypes() { + given(m1.type()).will(inv -> String.class); + given(m2.type()).will(inv -> Integer.class); + + assertThat(and.type()).isEqualTo(Void.class); + } + + @Test + public void shouldReturnLeftBaseType() { + given(m1.type()).will(inv -> BaseType.class); + given(m2.type()).will(inv -> SubType.class); + + assertThat(and.type()).isEqualTo(BaseType.class); + } + + @Test + public void shouldReturnRightBaseType() { + given(m1.type()).will(inv -> SubType.class); + given(m2.type()).will(inv -> BaseType.class); + + assertThat(and.type()).isEqualTo(BaseType.class); + } + + private interface BaseType {} + + private interface SubType extends BaseType {} +} diff --git a/src/test/java/org/mockito/internal/matchers/CapturingMatcherTest.java b/src/test/java/org/mockito/internal/matchers/CapturingMatcherTest.java index a4c6b59149..768d08d0b8 100644 --- a/src/test/java/org/mockito/internal/matchers/CapturingMatcherTest.java +++ b/src/test/java/org/mockito/internal/matchers/CapturingMatcherTest.java @@ -19,7 +19,7 @@ public class CapturingMatcherTest extends TestBase { @Test public void should_capture_arguments() throws Exception { // given - CapturingMatcher m = new CapturingMatcher(); + CapturingMatcher m = new CapturingMatcher(String.class); // when m.captureFrom("foo"); @@ -32,7 +32,7 @@ public void should_capture_arguments() throws Exception { @Test public void should_know_last_captured_value() throws Exception { // given - CapturingMatcher m = new CapturingMatcher(); + CapturingMatcher m = new CapturingMatcher(String.class); // when m.captureFrom("foo"); @@ -45,7 +45,7 @@ public void should_know_last_captured_value() throws Exception { @Test public void should_scream_when_nothing_yet_captured() throws Exception { // given - CapturingMatcher m = new CapturingMatcher(); + CapturingMatcher m = new CapturingMatcher(String.class); try { // when @@ -59,7 +59,7 @@ public void should_scream_when_nothing_yet_captured() throws Exception { @Test public void should_not_fail_when_used_in_concurrent_tests() throws Exception { // given - final CapturingMatcher m = new CapturingMatcher(); + final CapturingMatcher m = new CapturingMatcher(String.class); // when m.captureFrom("concurrent access"); diff --git a/src/test/java/org/mockito/internal/matchers/EqualsTest.java b/src/test/java/org/mockito/internal/matchers/EqualsTest.java index ba5c578b57..e56de2a9c2 100644 --- a/src/test/java/org/mockito/internal/matchers/EqualsTest.java +++ b/src/test/java/org/mockito/internal/matchers/EqualsTest.java @@ -4,9 +4,11 @@ */ package org.mockito.internal.matchers; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.*; import org.junit.Test; +import org.mockito.ArgumentMatcher; import org.mockitoutil.TestBase; public class EqualsTest extends TestBase { @@ -102,4 +104,15 @@ public void shouldMatchTypesSafelyWhenGivenIsNull() throws Exception { // then assertFalse(equals.typeMatches(null)); } + + @Test + public void shouldInferType() { + assertThat(new Equals("String").type()).isEqualTo(String.class); + } + + @Test + public void shouldDefaultTypeOnNull() { + assertThat(new Equals(null).type()) + .isEqualTo(((ArgumentMatcher) argument -> false).type()); + } } diff --git a/src/test/java/org/mockito/internal/matchers/OrTest.java b/src/test/java/org/mockito/internal/matchers/OrTest.java new file mode 100644 index 0000000000..6738d73507 --- /dev/null +++ b/src/test/java/org/mockito/internal/matchers/OrTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.matchers; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockitoutil.TestBase; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +public class OrTest extends TestBase { + + @Mock private ArgumentMatcher m1; + @Mock private ArgumentMatcher m2; + private Or or; + + @Before + public void setUp() throws Exception { + or = new Or(m1, m2); + } + + @Test + public void shouldReturnMatchingTypes() { + given(m1.type()).will(inv -> String.class); + given(m2.type()).will(inv -> String.class); + + assertThat(or.type()).isEqualTo(String.class); + } + + @Test + public void shouldDefaultMismatchingTypes() { + given(m1.type()).will(inv -> String.class); + given(m2.type()).will(inv -> Integer.class); + + assertThat(or.type()).isEqualTo(Void.class); + } + + @Test + public void shouldReturnLeftBaseType() { + given(m1.type()).will(inv -> BaseType.class); + given(m2.type()).will(inv -> SubType.class); + + assertThat(or.type()).isEqualTo(BaseType.class); + } + + @Test + public void shouldReturnRightBaseType() { + given(m1.type()).will(inv -> SubType.class); + given(m2.type()).will(inv -> BaseType.class); + + assertThat(or.type()).isEqualTo(BaseType.class); + } + + private interface BaseType {} + + private interface SubType extends BaseType {} +} diff --git a/src/test/java/org/mockito/internal/matchers/SameTest.java b/src/test/java/org/mockito/internal/matchers/SameTest.java new file mode 100644 index 0000000000..d5c6a1f9c8 --- /dev/null +++ b/src/test/java/org/mockito/internal/matchers/SameTest.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.internal.matchers; + +import org.mockitoutil.TestBase; +import org.junit.Test; +import org.mockito.ArgumentMatcher; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SameTest extends TestBase { + + @Test + public void shouldInferType() { + assertThat(new Same("String").type()).isEqualTo(String.class); + } + + @Test + public void shouldDefaultTypeOnNull() { + assertThat(new Same(null).type()) + .isEqualTo(((ArgumentMatcher) argument -> false).type()); + } +} diff --git a/src/test/java/org/mockitousage/IMethods.java b/src/test/java/org/mockitousage/IMethods.java index 06492f09cb..5c1d3d8e5d 100644 --- a/src/test/java/org/mockitousage/IMethods.java +++ b/src/test/java/org/mockitousage/IMethods.java @@ -187,10 +187,12 @@ String sixArgumentVarArgsMethod( int varargs(Object... object); - String varargsReturningString(Object... object); - int varargs(String... string); + int polyVararg(BaseType... args); + + String varargsReturningString(Object... object); + void mixedVarargs(Object i, String... string); String mixedVarargsReturningString(Object i, String... string); @@ -199,6 +201,10 @@ String sixArgumentVarArgsMethod( Object[] mixedVarargsReturningObjectArray(Object i, String... string); + String methodWithVarargAndNonVarargVariants(String string); + + String methodWithVarargAndNonVarargVariants(String... string); + List listReturningMethod(Object... objects); LinkedList linkedListReturningMethod(); @@ -306,4 +312,6 @@ public int hashCode() { return Objects.hash(value); } } + + interface BaseType {} } diff --git a/src/test/java/org/mockitousage/MethodsImpl.java b/src/test/java/org/mockitousage/MethodsImpl.java index e2d53ba7ba..678d3f7fe1 100644 --- a/src/test/java/org/mockitousage/MethodsImpl.java +++ b/src/test/java/org/mockitousage/MethodsImpl.java @@ -354,14 +354,19 @@ public int varargs(Object... object) { return -1; } - public String varargsReturningString(Object... object) { - return null; - } - public int varargs(String... string) { return -1; } + @Override + public int polyVararg(final BaseType... args) { + return 0; + } + + public String varargsReturningString(Object... object) { + return null; + } + public void mixedVarargs(Object i, String... string) {} public String mixedVarargsReturningString(Object i, String... string) { @@ -376,6 +381,16 @@ public Object[] mixedVarargsReturningObjectArray(Object i, String... string) { return null; } + @Override + public String methodWithVarargAndNonVarargVariants(String string) { + return "plain"; + } + + @Override + public String methodWithVarargAndNonVarargVariants(String... string) { + return "varargs"; + } + public void varargsbyte(byte... bytes) {} public List listReturningMethod(Object... objects) { diff --git a/src/test/java/org/mockitousage/matchers/VarargsTest.java b/src/test/java/org/mockitousage/matchers/VarargsTest.java index dfb726942b..0e5456fd2f 100644 --- a/src/test/java/org/mockitousage/matchers/VarargsTest.java +++ b/src/test/java/org/mockitousage/matchers/VarargsTest.java @@ -4,12 +4,18 @@ */ package org.mockitousage.matchers; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.fail; +import static org.mockito.AdditionalMatchers.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.isNotNull; import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -28,6 +34,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.mockitousage.IMethods; +import org.mockitousage.IMethods.BaseType; public class VarargsTest { @@ -52,27 +59,17 @@ public void shouldMatchVarArgs_noArgs() { } @Test - @Ignore("This test must succeed but is fails currently, see github issue #616") public void shouldMatchEmptyVarArgs_noArgsIsNotNull() { mock.varargs(); - verify(mock).varargs(isNotNull()); + verify(mock).varargs(isNotNull(String[].class)); } @Test - @Ignore("This test must succeed but is fails currently, see github issue #616") public void shouldMatchEmptyVarArgs_noArgsIsNull() { - mock.varargs(); + mock.varargs((String[]) null); - verify(mock).varargs(isNull()); - } - - @Test - @Ignore("This test must succeed but is fails currently, see github issue #616") - public void shouldMatchEmptyVarArgs_noArgsIsNotNullArray() { - mock.varargs(); - - verify(mock).varargs((String[]) isNotNull()); + verify(mock).varargs(isNull(String[].class)); } @Test @@ -178,11 +175,10 @@ public void shouldMatchVarArgs_emptyByteArray() { } @Test - @Ignore public void shouldMatchEmptyVarArgs_emptyArrayIsNotNull() { mock.varargsbyte(); - verify(mock).varargsbyte((byte[]) isNotNull()); + verify(mock).varargsbyte(isNotNull(byte[].class)); } @Test @@ -198,7 +194,7 @@ public void shouldCaptureVarArgs_noArgs() { verify(mock).varargs(captor.capture()); - assertThat(captor).isEmpty(); + assertThatCaptor(captor).isEmpty(); } @Test @@ -208,7 +204,7 @@ public void shouldCaptureVarArgs_oneNullArg_eqNull() { verify(mock).varargs(captor.capture()); - assertThat(captor).areExactly(1, NULL); + assertThatCaptor(captor).areExactly(1, NULL); } /** @@ -221,7 +217,7 @@ public void shouldCaptureVarArgs_nullArrayArg() { mock.varargs(argArray); verify(mock).varargs(captor.capture()); - assertThat(captor).areExactly(1, NULL); + assertThatCaptor(captor).areExactly(1, NULL); } @Test @@ -230,7 +226,7 @@ public void shouldCaptureVarArgs_twoArgsOneCapture() { verify(mock).varargs(captor.capture()); - assertThat(captor).contains("1", "2"); + assertThatCaptor(captor).contains("1", "2"); } @Test @@ -239,7 +235,7 @@ public void shouldCaptureVarArgs_twoArgsTwoCaptures() { verify(mock).varargs(captor.capture(), captor.capture()); - assertThat(captor).contains("1", "2"); + assertThatCaptor(captor).contains("1", "2"); } @Test @@ -248,7 +244,7 @@ public void shouldCaptureVarArgs_oneNullArgument() { verify(mock).varargs(captor.capture()); - assertThat(captor).contains("1", (String) null); + assertThatCaptor(captor).contains("1", (String) null); } @Test @@ -257,7 +253,7 @@ public void shouldCaptureVarArgs_oneNullArgument2() { verify(mock).varargs(captor.capture(), captor.capture()); - assertThat(captor).contains("1", (String) null); + assertThatCaptor(captor).contains("1", (String) null); } @Test @@ -277,7 +273,7 @@ public void shouldCaptureVarArgs_3argsCaptorMatcherMix() { verify(mock).varargs(captor.capture(), eq("2"), captor.capture()); - assertThat(captor).containsExactly("1", "3"); + assertThatCaptor(captor).containsExactly("1", "3"); } @Test @@ -290,7 +286,7 @@ public void shouldNotCaptureVarArgs_3argsCaptorMatcherMix() { } catch (ArgumentsAreDifferent expected) { } - assertThat(captor).isEmpty(); + assertThatCaptor(captor).isEmpty(); } @Test @@ -304,16 +300,7 @@ public void shouldNotCaptureVarArgs_1args2captures() { .isInstanceOf(ArgumentsAreDifferent.class); } - /** - * As of v2.0.0-beta.118 this test fails. Once the github issues: - *
    - *
  • '#584 ArgumentCaptor can't capture varargs-arrays - *
  • #565 ArgumentCaptor should be type aware' are fixed this test must - * succeed - *
- */ @Test - @Ignore("Blocked by github issue: #584 & #565") public void shouldCaptureVarArgsAsArray() { mock.varargs("1", "2"); @@ -321,7 +308,7 @@ public void shouldCaptureVarArgsAsArray() { verify(mock).varargs(varargCaptor.capture()); - assertThat(varargCaptor).containsExactly(new String[] {"1", "2"}); + assertThatCaptor(varargCaptor).containsExactly(new String[] {"1", "2"}); } @Test @@ -342,8 +329,281 @@ public void shouldNotMatchVaraArgs() { Assertions.assertThat(mock.varargsObject(1)).isNull(); } - private static AbstractListAssert> assertThat( + @Test + public void shouldDifferentiateNonVarargVariant() { + given(mock.methodWithVarargAndNonVarargVariants(any(String.class))) + .willReturn("single arg method"); + + assertThat(mock.methodWithVarargAndNonVarargVariants("a")).isEqualTo("single arg method"); + assertThat(mock.methodWithVarargAndNonVarargVariants(new String[] {"a"})).isNull(); + assertThat(mock.methodWithVarargAndNonVarargVariants("a", "b")).isNull(); + } + + @Test + public void shouldMockVarargsInvocation_single_vararg_matcher() { + given(mock.methodWithVarargAndNonVarargVariants(any(String[].class))) + .willReturn("var arg method"); + + assertThat(mock.methodWithVarargAndNonVarargVariants("a")).isNull(); + assertThat(mock.methodWithVarargAndNonVarargVariants(new String[] {"a"})) + .isEqualTo("var arg method"); + assertThat(mock.methodWithVarargAndNonVarargVariants("a", "b")).isEqualTo("var arg method"); + } + + @Test + public void shouldMockVarargsInvocation_multiple_vararg_matcher() { + given(mock.methodWithVarargAndNonVarargVariants(any(String.class), any(String.class))) + .willReturn("var arg method"); + + assertThat(mock.methodWithVarargAndNonVarargVariants("a")).isNull(); + assertThat(mock.methodWithVarargAndNonVarargVariants(new String[] {"a"})).isNull(); + assertThat(mock.methodWithVarargAndNonVarargVariants("a", "b")).isEqualTo("var arg method"); + assertThat(mock.methodWithVarargAndNonVarargVariants(new String[] {"a", "b"})) + .isEqualTo("var arg method"); + assertThat(mock.methodWithVarargAndNonVarargVariants("a", "b", "c")).isNull(); + } + + @Test + public void shouldMockVarargsInvocationUsingCasts() { + given(mock.methodWithVarargAndNonVarargVariants((String) any())) + .willReturn("single arg method"); + given(mock.methodWithVarargAndNonVarargVariants((String[]) any())) + .willReturn("var arg method"); + + assertThat(mock.methodWithVarargAndNonVarargVariants("a")).isEqualTo("single arg method"); + assertThat(mock.methodWithVarargAndNonVarargVariants()).isEqualTo("var arg method"); + assertThat(mock.methodWithVarargAndNonVarargVariants(new String[] {"a"})) + .isEqualTo("var arg method"); + assertThat(mock.methodWithVarargAndNonVarargVariants("a", "b")).isEqualTo("var arg method"); + } + + @Test + public void shouldMockVarargsInvocationForSuperType() { + given(mock.varargsReturningString(any(Object[].class))).willReturn("a"); + + assertThat(mock.varargsReturningString("a", "b")).isEqualTo("a"); + } + + @Test + public void shouldHandleArrayVarargsMethods() { + given(mock.arrayVarargsMethod(any(String[][].class))).willReturn(1); + + assertThat(mock.arrayVarargsMethod(new String[] {})).isEqualTo(1); + } + + @Test + public void shouldCaptureVarArgs_NullArrayArg1() { + mock.varargs((String[]) null); + ArgumentCaptor captor = ArgumentCaptor.forClass(String[].class); + + verify(mock).varargs(captor.capture()); + + assertThat(captor.getValue()).isNull(); + } + + @Test + public void shouldCaptureVarArgs_NullArrayArg2() { + mock.varargs((String[]) null); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + + verify(mock).varargs(captor.capture()); + + assertThat(captor.getValue()).isNull(); + } + + @Test + public void shouldVerifyVarArgs_any_NullArrayArg1() { + mock.varargs((String[]) null); + + verify(mock).varargs(any()); + } + + @Test + public void shouldVerifyVarArgs_any_NullArrayArg2() { + mock.varargs((String) null); + + verify(mock).varargs(any()); + } + + @Test + public void shouldVerifyVarArgs_eq_NullArrayArg1() { + mock.varargs((String[]) null); + + verify(mock).varargs(eq(null)); + } + + @Test + public void shouldVerifyVarArgs_eq_NullArrayArg2() { + mock.varargs((String) null); + + verify(mock).varargs(eq(null)); + } + + @Test + public void shouldVerifyVarArgs_isNull_NullArrayArg() { + mock.varargs((String) null); + + verify(mock).varargs(isNull(String.class)); + } + + @Test + public void shouldVerifyVarArgs_isNull_NullArrayArg2() { + mock.varargs((String) null); + + verify(mock).varargs(isNull()); + } + + @Test + public void shouldVerifyExactlyOneVarArg_isA() { + mock.varargs("one param"); + + verify(mock).varargs(isA(String.class)); + } + + @Test + public void shouldNotVerifyExactlyOneVarArg_isA() { + mock.varargs("two", "params"); + + verify(mock, never()).varargs(isA(String.class)); + } + + @Test + public void shouldVerifyVarArgArray_isA() { + mock.varargs("one param"); + + verify(mock).varargs(isA(String[].class)); + } + + @Test + public void shouldVerifyVarArgArray_isA2() { + mock.varargs("two", "params"); + + verify(mock).varargs(isA(String[].class)); + } + + @Test + public void shouldVerifyExactlyOneVarArg_any() { + mock.varargs("one param"); + + verify(mock).varargs(any(String.class)); + } + + @Test + @Ignore("Fails due to https://github.com/mockito/mockito/issues/1593") + public void shouldNotVerifyExactlyOneVarArg_any() { + mock.varargs("two", "params"); + + verify(mock, never()).varargs(any(String.class)); + } + + @Test + public void shouldMockVarargInvocation_eq() { + given(mock.varargs(eq("one param"))).willReturn(1); + + assertThat(mock.varargs("one param")).isEqualTo(1); + assertThat(mock.varargs()).isEqualTo(0); + assertThat(mock.varargs("different")).isEqualTo(0); + assertThat(mock.varargs("one param", "another")).isEqualTo(0); + } + + @Test + public void shouldVerifyInvocation_eq() { + mock.varargs("one param"); + + verify(mock).varargs(eq("one param")); + verify(mock, never()).varargs(); + verify(mock, never()).varargs(eq("different")); + verify(mock, never()).varargs(eq("one param"), eq("another")); + } + + @Test + public void shouldMockVarargInvocation_eq_raw() { + given(mock.varargs(eq(new String[] {"one param"}))).willReturn(1); + + assertThat(mock.varargs("one param")).isEqualTo(1); + assertThat(mock.varargs()).isEqualTo(0); + assertThat(mock.varargs("different")).isEqualTo(0); + assertThat(mock.varargs("one param", "another")).isEqualTo(0); + } + + @Test + public void shouldVerifyInvocation_eq_raw() { + mock.varargs("one param"); + + verify(mock).varargs(eq(new String[] {"one param"})); + verify(mock, never()).varargs(eq(new String[] {})); + verify(mock, never()).varargs(eq(new String[] {"different"})); + verify(mock, never()).varargs(eq(new String[] {"one param", "another"})); + } + + @Test + public void shouldVerifyInvocation_not() { + mock.varargs("one param"); + + verify(mock).varargs(not(eq(new String[] {"diff"}))); + verify(mock, never()).varargs(not(eq(new String[] {"one param"}))); + } + + @Test + public void shouldVerifyInvocation_same() { + String[] args = {"two", "params"}; + + mock.varargs(args); + + verify(mock).varargs(same(args)); + verify(mock, never()).varargs(same(new String[] {"two", "params"})); + } + + @Test + public void shouldVerifySubTypes() { + mock.polyVararg(new SubType(), new SubType()); + + verify(mock).polyVararg(eq(new SubType()), eq(new SubType())); + verify(mock).polyVararg(eq(new SubType[] {new SubType(), new SubType()})); + verify(mock).polyVararg(eq(new BaseType[] {new SubType(), new SubType()})); + } + + @Test + public void shouldVerifyInvocation_or() { + mock.polyVararg(new SubType(), new SubType()); + + verify(mock) + .polyVararg( + or( + eq(new BaseType[] {new SubType()}), + eq(new SubType[] {new SubType(), new SubType()}))); + verify(mock) + .polyVararg( + or( + eq(new BaseType[] {new SubType(), new SubType()}), + eq(new SubType[] {new SubType()}))); + } + + @Test + public void shouldVerifyInvocation_and() { + mock.polyVararg(new SubType(), new SubType()); + + verify(mock) + .polyVararg( + and( + eq(new BaseType[] {new SubType(), new SubType()}), + eq(new SubType[] {new SubType(), new SubType()}))); + } + + private static AbstractListAssert> assertThatCaptor( ArgumentCaptor captor) { return Assertions.assertThat(captor.getAllValues()); } + + private static class SubType implements BaseType { + @Override + public boolean equals(final Object obj) { + return obj != null && obj.getClass().equals(getClass()); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + } }