From 44cada251132c5d60bbbcf166ec0c58574e2c9cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oscar=20Guill=C3=A9n?= Date: Fri, 23 Feb 2024 19:43:32 +0100 Subject: [PATCH] Add support for destructured parameters in answers (#512) --- .../org/mockito/kotlin/KInvocationOnMock.kt | 39 +++++++++++++++++ .../org/mockito/kotlin/OngoingStubbing.kt | 8 ++-- .../org/mockito/kotlin/internal/KAnswer.kt | 42 +++++++++++++++++++ .../kotlin/internal/SuspendableAnswer.kt | 5 ++- .../src/test/kotlin/test/CoroutinesTest.kt | 15 +++++-- .../test/kotlin/test/OngoingStubbingTest.kt | 30 +++++++++++++ 6 files changed, 129 insertions(+), 10 deletions(-) create mode 100644 mockito-kotlin/src/main/kotlin/org/mockito/kotlin/KInvocationOnMock.kt create mode 100644 mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/KAnswer.kt diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/KInvocationOnMock.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/KInvocationOnMock.kt new file mode 100644 index 00000000..b433b761 --- /dev/null +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/KInvocationOnMock.kt @@ -0,0 +1,39 @@ +/* + * The MIT License + * + * Copyright (c) 2018 Niek Haarman + * Copyright (c) 2007 Mockito contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.mockito.kotlin + +import org.mockito.invocation.InvocationOnMock + +class KInvocationOnMock( + private val invocationOnMock: InvocationOnMock +) : InvocationOnMock by invocationOnMock { + + operator fun component1(): T = invocationOnMock.getArgument(0) + operator fun component2(): T = invocationOnMock.getArgument(1) + operator fun component3(): T = invocationOnMock.getArgument(2) + operator fun component4(): T = invocationOnMock.getArgument(3) + operator fun component5(): T = invocationOnMock.getArgument(4) +} diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/OngoingStubbing.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/OngoingStubbing.kt index 30966bf3..39c6dcb2 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/OngoingStubbing.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/OngoingStubbing.kt @@ -28,7 +28,7 @@ package org.mockito.kotlin import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.runBlocking import org.mockito.Mockito -import org.mockito.invocation.InvocationOnMock +import org.mockito.kotlin.internal.KAnswer import org.mockito.kotlin.internal.SuspendableAnswer import org.mockito.stubbing.Answer import org.mockito.stubbing.OngoingStubbing @@ -132,10 +132,10 @@ infix fun OngoingStubbing.doAnswer(answer: Answer<*>): OngoingStubbing /** * Sets a generic Answer for the method using a lambda. */ -infix fun OngoingStubbing.doAnswer(answer: (InvocationOnMock) -> T?): OngoingStubbing { - return thenAnswer(answer) +infix fun OngoingStubbing.doAnswer(answer: (KInvocationOnMock) -> T?): OngoingStubbing { + return thenAnswer(KAnswer(answer)) } -infix fun OngoingStubbing.doSuspendableAnswer(answer: suspend (InvocationOnMock) -> T?): OngoingStubbing { +infix fun OngoingStubbing.doSuspendableAnswer(answer: suspend (KInvocationOnMock) -> T?): OngoingStubbing { return thenAnswer(SuspendableAnswer(answer)) } diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/KAnswer.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/KAnswer.kt new file mode 100644 index 00000000..ca62f048 --- /dev/null +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/KAnswer.kt @@ -0,0 +1,42 @@ +/* + * The MIT License + * + * Copyright (c) 2018 Niek Haarman + * Copyright (c) 2007 Mockito contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.mockito.kotlin.internal + +import org.mockito.invocation.InvocationOnMock +import org.mockito.kotlin.KInvocationOnMock +import org.mockito.stubbing.Answer + +/** + * This class wraps destructuring lambda into [Answer] + */ +@Suppress("UNCHECKED_CAST") +internal class KAnswer( + private val body: (KInvocationOnMock) -> T? +) : Answer { + override fun answer(invocation: InvocationOnMock): T { + return body(KInvocationOnMock(invocation)) as T + } +} diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/SuspendableAnswer.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/SuspendableAnswer.kt index 3544cf68..239be2cd 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/SuspendableAnswer.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/SuspendableAnswer.kt @@ -27,6 +27,7 @@ package org.mockito.kotlin.internal import org.mockito.internal.invocation.InterceptedInvocation import org.mockito.invocation.InvocationOnMock +import org.mockito.kotlin.KInvocationOnMock import org.mockito.stubbing.Answer import kotlin.coroutines.Continuation import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn @@ -36,7 +37,7 @@ import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn */ @Suppress("UNCHECKED_CAST") internal class SuspendableAnswer( - private val body: suspend (InvocationOnMock) -> T? + private val body: suspend (KInvocationOnMock) -> T? ) : Answer { override fun answer(invocation: InvocationOnMock?): T { //all suspend functions/lambdas has Continuation as the last argument. @@ -45,6 +46,6 @@ internal class SuspendableAnswer( val continuation = rawInvocation.rawArguments.last() as Continuation // https://youtrack.jetbrains.com/issue/KT-33766#focus=Comments-27-3707299.0-0 - return body.startCoroutineUninterceptedOrReturn(invocation, continuation) as T + return body.startCoroutineUninterceptedOrReturn(KInvocationOnMock(invocation), continuation) as T } } diff --git a/mockito-kotlin/src/test/kotlin/test/CoroutinesTest.kt b/mockito-kotlin/src/test/kotlin/test/CoroutinesTest.kt index eea7fd43..363c2230 100644 --- a/mockito-kotlin/src/test/kotlin/test/CoroutinesTest.kt +++ b/mockito-kotlin/src/test/kotlin/test/CoroutinesTest.kt @@ -3,10 +3,6 @@ package test import com.nhaarman.expect.expect -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext import kotlinx.coroutines.* import kotlinx.coroutines.channels.actor import org.junit.Assert.assertEquals @@ -265,6 +261,17 @@ class CoroutinesTest { assertEquals(42, fixture.suspending()) } + @Test + fun answerWithSuspendFunctionWithDestructuredArgs() = runBlocking { + val fixture: SomeInterface = mock() + + whenever(fixture.suspendingWithArg(any())).doSuspendableAnswer { (i: Int) -> + withContext(Dispatchers.Default) { i } + } + + assertEquals(5, fixture.suspendingWithArg(5)) + } + @Test fun willAnswerWithControlledSuspend() = runBlocking { val fixture: SomeInterface = mock() diff --git a/tests/src/test/kotlin/test/OngoingStubbingTest.kt b/tests/src/test/kotlin/test/OngoingStubbingTest.kt index ab452a76..ba1bd328 100644 --- a/tests/src/test/kotlin/test/OngoingStubbingTest.kt +++ b/tests/src/test/kotlin/test/OngoingStubbingTest.kt @@ -201,6 +201,36 @@ class OngoingStubbingTest : TestBase() { expect(result).toBe("argument-result") } + @Test + fun testOngoingStubbing_doAnswer_withDestructuredArgument() { + /* Given */ + val mock = mock { + on { stringResult(any()) } doAnswer { (s: String) -> "$s-result" } + } + + /* When */ + val result = mock.stringResult("argument") + + /* Then */ + expect(result).toBe("argument-result") + } + + @Test + fun testOngoingStubbing_doAnswer_withDestructuredArguments() { + /* Given */ + val mock = mock { + on { varargBooleanResult(any(), any()) } doAnswer { (a: String, b: String) -> + a == b.trim() + } + } + + /* When */ + val result = mock.varargBooleanResult("argument", " argument ") + + /* Then */ + expect(result).toBe(true) + } + @Test fun testMockStubbingAfterCreatingMock() { val mock = mock()