From 459efadf3774e1e7f65a5becc3ffb1cad4e20039 Mon Sep 17 00:00:00 2001 From: Big Andy <8012398+big-andy-coates@users.noreply.github.com> Date: Mon, 6 Jun 2022 18:00:10 +0100 Subject: [PATCH] Improve Varargs handling in AdditionalAnswers Fixes: #2644 Fixes issues around vararg handling for the following methods in `AdditionalAnswers`: * `returnsFirstArg` * `returnsSecondArg` * `returnsLastArg` * `returnsArgAt` * `answer` * `answerVoid` These methods were not correctly handling varargs. For example, ```java doAnswer(answerVoid( (VoidAnswer2) logger::info )).when(mock) .info(any(), (Object[]) any()); mock.info("Some message with {} {} {}", "three", "parameters", ""); ``` Would previously have resulted in a `ClassCastException` being thrown from the `mock.info` call. This was because the `answerVoid` method was not taking into account that the second parameter was a varargs parameter and was attempting to pass the second actual argument `"three"`, rather than the second _raw_ argument `["three", "parameters", ""]`. --- .../answers/AnswerFunctionalInterfaces.java | 80 +++- .../stubbing/answers/ReturnsArgumentAt.java | 52 ++- src/test/java/org/mockitousage/IMethods.java | 20 + .../java/org/mockitousage/MethodsImpl.java | 42 ++ .../StubbingWithAdditionalAnswersTest.java | 371 ++++++++++++++++++ 5 files changed, 535 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/mockito/internal/stubbing/answers/AnswerFunctionalInterfaces.java b/src/main/java/org/mockito/internal/stubbing/answers/AnswerFunctionalInterfaces.java index e4692f26d9..ac5dd3c72b 100644 --- a/src/main/java/org/mockito/internal/stubbing/answers/AnswerFunctionalInterfaces.java +++ b/src/main/java/org/mockito/internal/stubbing/answers/AnswerFunctionalInterfaces.java @@ -4,6 +4,8 @@ */ package org.mockito.internal.stubbing.answers; +import org.mockito.exceptions.base.MockitoException; +import org.mockito.invocation.Invocation; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer1; @@ -19,6 +21,10 @@ import org.mockito.stubbing.VoidAnswer5; import org.mockito.stubbing.VoidAnswer6; +import java.lang.reflect.Method; + +import static org.mockito.internal.util.StringUtil.join; + /** * Functional interfaces to make it easy to implement answers in Java 8 * @@ -38,11 +44,11 @@ private AnswerFunctionalInterfaces() {} * @return a new answer object */ public static Answer toAnswer(final Answer1 answer) { + final Method answerMethod = findAnswerMethod(answer.getClass(), 1); return new Answer() { @Override - @SuppressWarnings("unchecked") public T answer(InvocationOnMock invocation) throws Throwable { - return answer.answer((A) invocation.getArgument(0)); + return answer.answer(lastParameter(invocation, answerMethod, 0)); } }; } @@ -54,11 +60,11 @@ public T answer(InvocationOnMock invocation) throws Throwable { * @return a new answer object */ public static Answer toAnswer(final VoidAnswer1 answer) { + final Method answerMethod = findAnswerMethod(answer.getClass(), 1); return new Answer() { @Override - @SuppressWarnings("unchecked") public Void answer(InvocationOnMock invocation) throws Throwable { - answer.answer((A) invocation.getArgument(0)); + answer.answer(lastParameter(invocation, answerMethod, 0)); return null; } }; @@ -73,11 +79,13 @@ public Void answer(InvocationOnMock invocation) throws Throwable { * @return a new answer object */ public static Answer toAnswer(final Answer2 answer) { + final Method answerMethod = findAnswerMethod(answer.getClass(), 2); return new Answer() { @Override @SuppressWarnings("unchecked") public T answer(InvocationOnMock invocation) throws Throwable { - return answer.answer((A) invocation.getArgument(0), (B) invocation.getArgument(1)); + return answer.answer( + (A) invocation.getArgument(0), lastParameter(invocation, answerMethod, 1)); } }; } @@ -90,11 +98,13 @@ public T answer(InvocationOnMock invocation) throws Throwable { * @return a new answer object */ public static Answer toAnswer(final VoidAnswer2 answer) { + final Method answerMethod = findAnswerMethod(answer.getClass(), 2); return new Answer() { @Override @SuppressWarnings("unchecked") public Void answer(InvocationOnMock invocation) throws Throwable { - answer.answer((A) invocation.getArgument(0), (B) invocation.getArgument(1)); + answer.answer( + (A) invocation.getArgument(0), lastParameter(invocation, answerMethod, 1)); return null; } }; @@ -110,6 +120,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { * @return a new answer object */ public static Answer toAnswer(final Answer3 answer) { + final Method answerMethod = findAnswerMethod(answer.getClass(), 3); return new Answer() { @Override @SuppressWarnings("unchecked") @@ -117,7 +128,7 @@ public T answer(InvocationOnMock invocation) throws Throwable { return answer.answer( (A) invocation.getArgument(0), (B) invocation.getArgument(1), - (C) invocation.getArgument(2)); + lastParameter(invocation, answerMethod, 2)); } }; } @@ -131,6 +142,7 @@ public T answer(InvocationOnMock invocation) throws Throwable { * @return a new answer object */ public static Answer toAnswer(final VoidAnswer3 answer) { + final Method answerMethod = findAnswerMethod(answer.getClass(), 3); return new Answer() { @Override @SuppressWarnings("unchecked") @@ -138,7 +150,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { answer.answer( (A) invocation.getArgument(0), (B) invocation.getArgument(1), - (C) invocation.getArgument(2)); + lastParameter(invocation, answerMethod, 2)); return null; } }; @@ -155,6 +167,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { * @return a new answer object */ public static Answer toAnswer(final Answer4 answer) { + final Method answerMethod = findAnswerMethod(answer.getClass(), 4); return new Answer() { @Override @SuppressWarnings("unchecked") @@ -163,7 +176,7 @@ public T answer(InvocationOnMock invocation) throws Throwable { (A) invocation.getArgument(0), (B) invocation.getArgument(1), (C) invocation.getArgument(2), - (D) invocation.getArgument(3)); + lastParameter(invocation, answerMethod, 3)); } }; } @@ -178,6 +191,7 @@ public T answer(InvocationOnMock invocation) throws Throwable { * @return a new answer object */ public static Answer toAnswer(final VoidAnswer4 answer) { + final Method answerMethod = findAnswerMethod(answer.getClass(), 4); return new Answer() { @Override @SuppressWarnings("unchecked") @@ -186,7 +200,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { (A) invocation.getArgument(0), (B) invocation.getArgument(1), (C) invocation.getArgument(2), - (D) invocation.getArgument(3)); + lastParameter(invocation, answerMethod, 3)); return null; } }; @@ -204,6 +218,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { * @return a new answer object */ public static Answer toAnswer(final Answer5 answer) { + final Method answerMethod = findAnswerMethod(answer.getClass(), 5); return new Answer() { @Override @SuppressWarnings("unchecked") @@ -213,7 +228,7 @@ public T answer(InvocationOnMock invocation) throws Throwable { (B) invocation.getArgument(1), (C) invocation.getArgument(2), (D) invocation.getArgument(3), - (E) invocation.getArgument(4)); + lastParameter(invocation, answerMethod, 4)); } }; } @@ -229,6 +244,7 @@ public T answer(InvocationOnMock invocation) throws Throwable { * @return a new answer object */ public static Answer toAnswer(final VoidAnswer5 answer) { + final Method answerMethod = findAnswerMethod(answer.getClass(), 5); return new Answer() { @Override @SuppressWarnings("unchecked") @@ -238,7 +254,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { (B) invocation.getArgument(1), (C) invocation.getArgument(2), (D) invocation.getArgument(3), - (E) invocation.getArgument(4)); + lastParameter(invocation, answerMethod, 4)); return null; } }; @@ -259,6 +275,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { */ public static Answer toAnswer( final Answer6 answer) { + final Method answerMethod = findAnswerMethod(answer.getClass(), 6); return new Answer() { @Override @SuppressWarnings("unchecked") @@ -269,7 +286,7 @@ public T answer(InvocationOnMock invocation) throws Throwable { (C) invocation.getArgument(2), (D) invocation.getArgument(3), (E) invocation.getArgument(4), - (F) invocation.getArgument(5)); + lastParameter(invocation, answerMethod, 5)); } }; } @@ -288,6 +305,7 @@ public T answer(InvocationOnMock invocation) throws Throwable { */ public static Answer toAnswer( final VoidAnswer6 answer) { + final Method answerMethod = findAnswerMethod(answer.getClass(), 6); return new Answer() { @Override @SuppressWarnings("unchecked") @@ -298,9 +316,43 @@ public Void answer(InvocationOnMock invocation) throws Throwable { (C) invocation.getArgument(2), (D) invocation.getArgument(3), (E) invocation.getArgument(4), - (F) invocation.getArgument(5)); + lastParameter(invocation, answerMethod, 5)); return null; } }; } + + private static Method findAnswerMethod(final Class type, final int numberOfParameters) { + for (final Method m : type.getDeclaredMethods()) { + if (!m.isBridge() + && m.getName().equals("answer") + && m.getParameterTypes().length == numberOfParameters) { + return m; + } + } + throw new IllegalStateException( + "Failed to find answer() method on the supplied class: " + + type.getName() + + ", with the supplied number of parameters: " + + numberOfParameters); + } + + @SuppressWarnings("unchecked") + private static A lastParameter( + InvocationOnMock invocationOnMock, Method answerMethod, int argumentIndex) { + final Method invocationMethod = invocationOnMock.getMethod(); + + if (invocationMethod.isVarArgs() + && invocationMethod.getParameterTypes().length == (argumentIndex + 1)) { + final Class invocationRawArgType = + invocationMethod.getParameterTypes()[argumentIndex]; + final Class answerRawArgType = answerMethod.getParameterTypes()[argumentIndex]; + if (answerRawArgType.isAssignableFrom(invocationRawArgType)) { + Invocation invocation = (Invocation) invocationOnMock; + return (A) invocation.getRawArguments()[argumentIndex]; + } + } + + return invocationOnMock.getArgument(argumentIndex); + } } diff --git a/src/main/java/org/mockito/internal/stubbing/answers/ReturnsArgumentAt.java b/src/main/java/org/mockito/internal/stubbing/answers/ReturnsArgumentAt.java index cd661152c3..2c5297106c 100644 --- a/src/main/java/org/mockito/internal/stubbing/answers/ReturnsArgumentAt.java +++ b/src/main/java/org/mockito/internal/stubbing/answers/ReturnsArgumentAt.java @@ -51,25 +51,27 @@ public ReturnsArgumentAt(int wantedArgumentPosition) { } @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - int argumentPosition = inferWantedArgumentPosition(invocation); - validateIndexWithinInvocationRange(invocation, argumentPosition); + public Object answer(InvocationOnMock invocationOnMock) throws Throwable { + Invocation invocation = (Invocation) invocationOnMock; - if (wantedArgIndexIsVarargAndSameTypeAsReturnType( - invocation.getMethod(), argumentPosition)) { + if (wantedArgIndexIsVarargAndSameTypeAsReturnType(invocation)) { // answer raw vararg array argument - return ((Invocation) invocation).getRawArguments()[argumentPosition]; + return invocation.getRawArguments()[invocation.getRawArguments().length - 1]; } + int argumentPosition = inferWantedArgumentPosition(invocation); + validateIndexWithinInvocationRange(invocation, argumentPosition); + // answer expanded argument at wanted position return invocation.getArgument(argumentPosition); } @Override - public void validateFor(InvocationOnMock invocation) { + public void validateFor(InvocationOnMock invocationOnMock) { + Invocation invocation = (Invocation) invocationOnMock; int argumentPosition = inferWantedArgumentPosition(invocation); - validateIndexWithinInvocationRange(invocation, argumentPosition); - validateArgumentTypeCompatibility((Invocation) invocation, argumentPosition); + validateIndexWithinTheoreticalInvocationRange(invocation, argumentPosition); + validateArgumentTypeCompatibility(invocation, argumentPosition); } private int inferWantedArgumentPosition(InvocationOnMock invocation) { @@ -80,9 +82,26 @@ private int inferWantedArgumentPosition(InvocationOnMock invocation) { return wantedArgumentPosition; } + private int inferWantedRawArgumentPosition(Invocation invocation) { + if (wantedArgumentPosition == LAST_ARGUMENT) { + return invocation.getRawArguments().length - 1; + } + + return wantedArgumentPosition; + } + private void validateIndexWithinInvocationRange( InvocationOnMock invocation, int argumentPosition) { - if (!wantedArgumentPositionIsValidForInvocation(invocation, argumentPosition)) { + + if (invocation.getArguments().length <= argumentPosition) { + throw invalidArgumentPositionRangeAtInvocationTime( + invocation, wantedArgumentPosition == LAST_ARGUMENT, wantedArgumentPosition); + } + } + + private void validateIndexWithinTheoreticalInvocationRange( + InvocationOnMock invocation, int argumentPosition) { + if (!wantedArgumentPositionIsValidForTheoreticalInvocation(invocation, argumentPosition)) { throw invalidArgumentPositionRangeAtInvocationTime( invocation, wantedArgumentPosition == LAST_ARGUMENT, wantedArgumentPosition); } @@ -102,15 +121,16 @@ private void validateArgumentTypeCompatibility(Invocation invocation, int argume } } - private boolean wantedArgIndexIsVarargAndSameTypeAsReturnType( - Method method, int argumentPosition) { + private boolean wantedArgIndexIsVarargAndSameTypeAsReturnType(Invocation invocation) { + int rawArgumentPosition = inferWantedRawArgumentPosition(invocation); + Method method = invocation.getMethod(); Class[] parameterTypes = method.getParameterTypes(); return method.isVarArgs() - && argumentPosition == /* vararg index */ parameterTypes.length - 1 - && method.getReturnType().isAssignableFrom(parameterTypes[argumentPosition]); + && rawArgumentPosition == /* vararg index */ parameterTypes.length - 1 + && method.getReturnType().isAssignableFrom(parameterTypes[rawArgumentPosition]); } - private boolean wantedArgumentPositionIsValidForInvocation( + private boolean wantedArgumentPositionIsValidForTheoreticalInvocation( InvocationOnMock invocation, int argumentPosition) { if (argumentPosition < 0) { return false; @@ -145,7 +165,7 @@ private Class inferArgumentType(Invocation invocation, int argumentIndex) { return parameterTypes[argumentIndex]; } // if wanted argument is vararg - if (wantedArgIndexIsVarargAndSameTypeAsReturnType(invocation.getMethod(), argumentIndex)) { + if (wantedArgIndexIsVarargAndSameTypeAsReturnType(invocation)) { // return the vararg array if return type is compatible // because the user probably want to return the array itself if the return type is // compatible diff --git a/src/test/java/org/mockitousage/IMethods.java b/src/test/java/org/mockitousage/IMethods.java index 12897422da..06492f09cb 100644 --- a/src/test/java/org/mockitousage/IMethods.java +++ b/src/test/java/org/mockitousage/IMethods.java @@ -133,8 +133,26 @@ String simpleMethod( String threeArgumentMethodWithStrings(int valueOne, String valueTwo, String valueThree); + String threeArgumentVarArgsMethod(int valueOne, String valueTwo, String... valueThree); + String fourArgumentMethod(int valueOne, String valueTwo, String valueThree, boolean[] array); + String fourArgumentVarArgsMethod( + int valueOne, String valueTwo, int valueThree, String... valueFour); + + String fiveArgumentVarArgsMethod( + int valueOne, String valueTwo, int valueThree, String valueFour, String... valueFive); + + String sixArgumentVarArgsMethod( + int valueOne, + String valueTwo, + int valueThree, + String valueFour, + String valueFive, + String... valueSix); + + int arrayVarargsMethod(String[]... arrayVarArgs); + void twoArgumentMethod(int one, int two); void arrayMethod(String[] strings); @@ -235,6 +253,8 @@ String simpleMethod( Integer toIntWrapper(int i); + Integer toIntWrapperVarArgs(int i, Object... varargs); + String forObject(Object object); String genericToString(T arg); diff --git a/src/test/java/org/mockitousage/MethodsImpl.java b/src/test/java/org/mockitousage/MethodsImpl.java index bb69656712..e2d53ba7ba 100644 --- a/src/test/java/org/mockitousage/MethodsImpl.java +++ b/src/test/java/org/mockitousage/MethodsImpl.java @@ -254,11 +254,48 @@ public String threeArgumentMethodWithStrings(int valueOne, String valueTwo, Stri return null; } + public String threeArgumentVarArgsMethod( + final int valueOne, final String valueTwo, final String... valueThree) { + return null; + } + public String fourArgumentMethod( int valueOne, String valueTwo, String valueThree, boolean[] array) { return null; } + public String fourArgumentVarArgsMethod( + final int valueOne, + final String valueTwo, + final int valueThree, + final String... valueFour) { + return null; + } + + public String fiveArgumentVarArgsMethod( + final int valueOne, + final String valueTwo, + final int valueThree, + final String valueFour, + final String... valueFive) { + return null; + } + + public String sixArgumentVarArgsMethod( + final int valueOne, + final String valueTwo, + final int valueThree, + final String valueFour, + final String valueFive, + final String... valueSix) { + return null; + } + + @Override + public int arrayVarargsMethod(final String[]... arrayVarArgs) { + return 0; + } + public void twoArgumentMethod(int one, int two) {} public void arrayMethod(String[] strings) {} @@ -435,6 +472,11 @@ public Integer toIntWrapper(int i) { return null; } + @Override + public Integer toIntWrapperVarArgs(final int i, final Object... varargs) { + return null; + } + public String forObject(Object object) { return null; } diff --git a/src/test/java/org/mockitousage/stubbing/StubbingWithAdditionalAnswersTest.java b/src/test/java/org/mockitousage/stubbing/StubbingWithAdditionalAnswersTest.java index 7dfed447a9..a6e8beecf6 100644 --- a/src/test/java/org/mockitousage/stubbing/StubbingWithAdditionalAnswersTest.java +++ b/src/test/java/org/mockitousage/stubbing/StubbingWithAdditionalAnswersTest.java @@ -5,6 +5,7 @@ package org.mockitousage.stubbing; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.within; import static org.mockito.AdditionalAnswers.answer; import static org.mockito.AdditionalAnswers.answerVoid; @@ -22,11 +23,13 @@ import static org.mockito.BDDMockito.times; import static org.mockito.BDDMockito.verify; +import java.util.Arrays; import java.util.Date; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.exceptions.base.MockitoException; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer1; import org.mockito.stubbing.Answer2; @@ -42,6 +45,7 @@ import org.mockito.stubbing.VoidAnswer6; import org.mockitousage.IMethods; +@SuppressWarnings({"Convert2Lambda", "Anonymous2MethodRef", "RedundantThrows"}) @RunWith(MockitoJUnitRunner.class) public class StubbingWithAdditionalAnswersTest { @@ -52,10 +56,39 @@ public void can_return_arguments_of_invocation() throws Exception { given(iMethods.objectArgMethod(any())).will(returnsFirstArg()); given(iMethods.threeArgumentMethod(eq(0), any(), anyString())).will(returnsSecondArg()); given(iMethods.threeArgumentMethod(eq(1), any(), anyString())).will(returnsLastArg()); + given(iMethods.mixedVarargsReturningString(eq(1), any())).will(returnsArgAt(2)); assertThat(iMethods.objectArgMethod("first")).isEqualTo("first"); assertThat(iMethods.threeArgumentMethod(0, "second", "whatever")).isEqualTo("second"); assertThat(iMethods.threeArgumentMethod(1, "whatever", "last")).isEqualTo("last"); + assertThat(iMethods.mixedVarargsReturningString(1, "a", "b")).isEqualTo("b"); + } + + @Test + public void can_return_var_arguments_of_invocation() throws Exception { + given(iMethods.mixedVarargsReturningStringArray(eq(1), any())).will(returnsLastArg()); + given(iMethods.mixedVarargsReturningObjectArray(eq(1), any())).will(returnsArgAt(1)); + + assertThat(iMethods.mixedVarargsReturningStringArray(1, "the", "var", "args")) + .containsExactlyInAnyOrder("the", "var", "args"); + assertThat(iMethods.mixedVarargsReturningObjectArray(1, "the", "var", "args")) + .containsExactlyInAnyOrder("the", "var", "args"); + } + + @Test + public void returns_arg_at_throws_on_out_of_range_var_args() throws Exception { + given(iMethods.mixedVarargsReturningString(eq(1), any())).will(returnsArgAt(3)); + + assertThatThrownBy(() -> iMethods.mixedVarargsReturningString(1, "a", "b")) + .isInstanceOf(MockitoException.class) + .hasMessageContaining("Invalid argument index"); + + assertThatThrownBy( + () -> + given(iMethods.mixedVarargsReturningStringArray(eq(1), any())) + .will(returnsArgAt(3))) + .isInstanceOf(MockitoException.class) + .hasMessageContaining("The argument of type 'String' cannot be returned"); } @Test @@ -85,9 +118,11 @@ public void can_return_expanded_arguments_of_invocation() throws Exception { public void can_return_primitives_or_wrappers() throws Exception { given(iMethods.toIntPrimitive(anyInt())).will(returnsFirstArg()); given(iMethods.toIntWrapper(anyInt())).will(returnsFirstArg()); + given(iMethods.toIntWrapperVarArgs(anyInt(), any())).will(returnsFirstArg()); assertThat(iMethods.toIntPrimitive(1)).isEqualTo(1); assertThat(iMethods.toIntWrapper(1)).isEqualTo(1); + assertThat(iMethods.toIntWrapperVarArgs(1, 10)).isEqualTo(1); } @Test @@ -347,4 +382,340 @@ public void answer( // expect the answer to write correctly to "target" verify(target, times(1)).simpleMethod("hello", 1, 2, 3, 4, 5); } + + @Test + public void can_return_based_on_strongly_types_one_parameter_var_args_function() + throws Exception { + given(iMethods.varargs(any())) + .will( + answer( + new Answer1() { + public Integer answer(String[] strings) { + return strings.length; + } + })); + + assertThat(iMethods.varargs("some", "args")).isEqualTo(2); + } + + @Test + public void will_execute_a_void_based_on_strongly_typed_one_parameter_var_args_function() + throws Exception { + final IMethods target = mock(IMethods.class); + + given(iMethods.varargs(any())) + .will( + answerVoid( + new VoidAnswer1() { + public void answer(String[] s) { + target.varargs(s); + } + })); + + // invoke on iMethods + iMethods.varargs("some", "args"); + + // expect the answer to write correctly to "target" + verify(target, times(1)).varargs("some", "args"); + } + + @Test + public void can_return_based_on_strongly_typed_two_parameter_var_args_function() + throws Exception { + given(iMethods.mixedVarargsReturningString(any(), any())) + .will( + answer( + new Answer2() { + public String answer(Object o, String[] s) { + return String.join("-", s); + } + })); + + assertThat(iMethods.mixedVarargsReturningString(1, "var", "args")).isEqualTo("var-args"); + } + + @Test + public void will_execute_a_void_based_on_strongly_typed_two_parameter_var_args_function() + throws Exception { + final IMethods target = mock(IMethods.class); + + given(iMethods.mixedVarargsReturningString(any(), any())) + .will( + answerVoid( + new VoidAnswer2() { + public void answer(Object o, String[] s) { + target.mixedVarargsReturningString(o, s); + } + })); + + // invoke on iMethods + iMethods.mixedVarargsReturningString(1, "var", "args"); + + // expect the answer to write correctly to "target" + verify(target).mixedVarargsReturningString(1, "var", "args"); + } + + @Test + public void can_return_based_on_strongly_typed_three_parameter_var_args_function() + throws Exception { + final IMethods target = mock(IMethods.class); + + given(iMethods.threeArgumentVarArgsMethod(anyInt(), any(), any())) + .will( + answer( + new Answer3() { + public String answer(Integer i, String s1, String[] s2) { + target.threeArgumentVarArgsMethod(i, s1, s2); + return String.join("-", s2); + } + })); + + // invoke on iMethods + assertThat(iMethods.threeArgumentVarArgsMethod(1, "string1", "var", "args")) + .isEqualTo("var-args"); + + // expect the answer to write correctly to "target" + verify(target).threeArgumentVarArgsMethod(1, "string1", "var", "args"); + } + + @Test + public void will_execute_a_void_based_on_strongly_typed_three_parameter_var_args_function() + throws Exception { + final IMethods target = mock(IMethods.class); + + given(iMethods.threeArgumentVarArgsMethod(anyInt(), any(), any())) + .will( + answerVoid( + new VoidAnswer3() { + public void answer(Integer i, String s1, String[] s2) { + target.threeArgumentVarArgsMethod(i, s1, s2); + } + })); + + // invoke on iMethods + iMethods.threeArgumentVarArgsMethod(1, "string1", "var", "args"); + + // expect the answer to write correctly to "target" + verify(target, times(1)).threeArgumentVarArgsMethod(1, "string1", "var", "args"); + } + + @Test + public void can_return_based_on_strongly_typed_four_parameter_var_args_function() + throws Exception { + final IMethods target = mock(IMethods.class); + given(iMethods.fourArgumentVarArgsMethod(anyInt(), any(), anyInt(), any())) + .will( + answer( + new Answer4() { + public String answer( + Integer i1, String s2, Integer i3, String[] s4) { + target.fourArgumentVarArgsMethod(i1, s2, i3, s4); + return String.join("-", s4); + } + })); + + // invoke on iMethods + String[] varargs = {"var", "args"}; + assertThat(iMethods.fourArgumentVarArgsMethod(1, "string1", 3, varargs)) + .isEqualTo("var-args"); + + // expect the answer to write correctly to "target" + verify(target, times(1)).fourArgumentVarArgsMethod(1, "string1", 3, varargs); + } + + @Test + public void will_execute_a_void_based_on_strongly_typed_four_parameter_var_args_function() + throws Exception { + final IMethods target = mock(IMethods.class); + + given(iMethods.fourArgumentVarArgsMethod(anyInt(), any(), anyInt(), any())) + .will( + answerVoid( + new VoidAnswer4() { + public void answer( + Integer i, String s2, Integer i3, String[] s4) { + target.fourArgumentVarArgsMethod(i, s2, i3, s4); + } + })); + + // invoke on iMethods + iMethods.fourArgumentVarArgsMethod(1, "string1", 3, "var", "args"); + + // expect the answer to write correctly to "target" + verify(target, times(1)).fourArgumentVarArgsMethod(1, "string1", 3, "var", "args"); + } + + @Test + public void can_return_based_on_strongly_typed_five_parameter_var_args_function() + throws Exception { + final IMethods target = mock(IMethods.class); + given(iMethods.fiveArgumentVarArgsMethod(anyInt(), any(), anyInt(), any(), any())) + .will( + answer( + new Answer5() { + public String answer( + Integer i1, + String s2, + Integer i3, + String s4, + String[] s5) { + target.fiveArgumentVarArgsMethod(i1, s2, i3, s4, s5); + return String.join("-", s5); + } + })); + + // invoke on iMethods + assertThat(iMethods.fiveArgumentVarArgsMethod(1, "two", 3, "four", "var", "args")) + .isEqualTo("var-args"); + + // expect the answer to write correctly to "target" + verify(target).fiveArgumentVarArgsMethod(1, "two", 3, "four", "var", "args"); + } + + @Test + public void will_execute_a_void_based_on_strongly_typed_five_parameter_var_args_function() + throws Exception { + final IMethods target = mock(IMethods.class); + + given(iMethods.fiveArgumentVarArgsMethod(anyInt(), any(), anyInt(), any(), any())) + .will( + answerVoid( + new VoidAnswer5() { + public void answer( + Integer i1, + String s2, + Integer i3, + String s4, + String[] s5) { + target.fiveArgumentVarArgsMethod(i1, s2, i3, s4, s5); + } + })); + + // invoke on iMethods + iMethods.fiveArgumentVarArgsMethod(1, "two", 3, "four", "var", "args"); + + // expect the answer to write correctly to "target" + verify(target).fiveArgumentVarArgsMethod(1, "two", 3, "four", "var", "args"); + } + + @Test + public void can_return_based_on_strongly_typed_six_parameter_var_args_function() + throws Exception { + final IMethods target = mock(IMethods.class); + given(iMethods.sixArgumentVarArgsMethod(anyInt(), any(), anyInt(), any(), any(), any())) + .will( + answer( + new Answer6< + String, + Integer, + String, + Integer, + String, + String, + String[]>() { + public String answer( + Integer i1, + String s2, + Integer i3, + String s4, + String s5, + String[] s6) { + target.sixArgumentVarArgsMethod(i1, s2, i3, s4, s5, s6); + return "answered"; + } + })); + + // invoke on iMethods + assertThat(iMethods.sixArgumentVarArgsMethod(1, "two", 3, "four", "five", "var", "args")) + .isEqualTo("answered"); + + // expect the answer to write correctly to "target" + verify(target, times(1)) + .sixArgumentVarArgsMethod(1, "two", 3, "four", "five", "var", "args"); + } + + @Test + public void will_execute_a_void_returning_strongly_typed_six_parameter_var_args_function() + throws Exception { + final IMethods target = mock(IMethods.class); + given(iMethods.sixArgumentVarArgsMethod(anyInt(), any(), anyInt(), any(), any(), any())) + .will( + answerVoid( + new VoidAnswer6< + Integer, String, Integer, String, String, String[]>() { + public void answer( + Integer i1, + String s2, + Integer i3, + String s4, + String s5, + String[] s6) { + target.sixArgumentVarArgsMethod(i1, s2, i3, s4, s5, s6); + } + })); + + // invoke on iMethods + iMethods.sixArgumentVarArgsMethod(1, "two", 3, "four", "five", "var", "args"); + + // expect the answer to write correctly to "target" + verify(target, times(1)) + .sixArgumentVarArgsMethod(1, "two", 3, "four", "five", "var", "args"); + } + + @Test + public void can_accept_array_supertype_for_strongly_typed_var_args_function() throws Exception { + given(iMethods.varargs(any())) + .will( + answer( + new Answer1() { + public Integer answer(Object[] s) { + return s.length; + } + })); + + assertThat(iMethods.varargs("var", "args")).isEqualTo(2); + } + + @Test + public void can_accept_non_vararg_answer_on_var_args_function() throws Exception { + given(iMethods.varargs(any())) + .will( + answer( + new Answer2() { + public Integer answer(String s1, String s2) { + return s1.length() + s2.length(); + } + })); + + assertThat(iMethods.varargs("var", "args")).isEqualTo(7); + } + + @Test + public void should_work_with_var_args_with_no_elements() throws Exception { + given(iMethods.varargs(any())) + .will( + answer( + new Answer1() { + public Integer answer(String[] s) { + return s.length; + } + })); + + assertThat(iMethods.varargs()).isEqualTo(0); + } + + @Test + public void should_work_with_array_var_args() throws Exception { + given(iMethods.arrayVarargsMethod(any())) + .will( + answer( + new Answer1() { + public Integer answer(String[][] s) { + return Arrays.stream(s).mapToInt(e -> e.length).sum(); + } + })); + + String[][] varArgs = {{}, {""}, {"", ""}}; + assertThat(iMethods.arrayVarargsMethod(varArgs)).isEqualTo(3); + } }