diff --git a/build.gradle b/build.gradle index 91d1f10..dd0a415 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ plugins { } def test = tasks.register("test") { - dependsOn gradle.includedBuild("tests").task(":test") + dependsOn gradle.includedBuild("tests").task(":test"), gradle.includedBuild("no-coroutine-tests").task(":test") } tasks.named("check") { dependsOn test diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/BDDMockito.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/BDDMockito.kt index b2ec6ae..3359709 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/BDDMockito.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/BDDMockito.kt @@ -37,6 +37,7 @@ import org.mockito.internal.stubbing.answers.ThrowsExceptionForClassType import org.mockito.invocation.InvocationOnMock import org.mockito.kotlin.internal.CoroutineAwareAnswer import org.mockito.kotlin.internal.CoroutineAwareAnswer.Companion.wrapAsCoroutineAwareAnswer +import org.mockito.kotlin.internal.safeRunBlocking import org.mockito.stubbing.Answer /** Alias for [BDDMockito.given]. */ @@ -66,7 +67,7 @@ fun then(mock: T): Then { /** Alias for [Then.should], with suspending lambda. */ fun Then.shouldBlocking(f: suspend T.() -> R): R { val m = should() - return runBlocking { m.f() } + return safeRunBlocking { m.f() } } /** Alias for [BDDMyOngoingStubbing.will] */ diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/KStubbing.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/KStubbing.kt index 1f65f01..a142021 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/KStubbing.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/KStubbing.kt @@ -26,11 +26,11 @@ package org.mockito.kotlin import kotlin.reflect.KClass -import kotlinx.coroutines.runBlocking import org.mockito.Mockito import org.mockito.Mockito.`when` import org.mockito.exceptions.misusing.NotAMockException import org.mockito.kotlin.internal.createInstance +import org.mockito.kotlin.internal.safeRunBlocking import org.mockito.stubbing.OngoingStubbing import org.mockito.stubbing.Stubber @@ -143,7 +143,7 @@ class KStubbing(val mock: T) { fun onGeneric(methodCall: suspend T.() -> R?, clazz: KClass): OngoingStubbing { val r = try { - runBlocking { mock.methodCall() } + safeRunBlocking { mock.methodCall() } } catch (_: NullPointerException) { // An NPE may be thrown by the Kotlin type system when the MockMethodInterceptor // returns a @@ -207,7 +207,7 @@ class KStubbing(val mock: T) { */ @Deprecated("Use on { methodCall } instead") fun KStubbing.onBlocking(methodCall: suspend T.() -> R): OngoingStubbing { - return runBlocking { `when`(mock.methodCall())!! } + return safeRunBlocking { `when`(mock.methodCall())!! } } /** 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 cb3a071..93badf3 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/OngoingStubbing.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/OngoingStubbing.kt @@ -37,6 +37,7 @@ import org.mockito.internal.stubbing.answers.ThrowsExceptionForClassType import org.mockito.kotlin.internal.CoroutineAwareAnswer import org.mockito.kotlin.internal.CoroutineAwareAnswer.Companion.wrapAsCoroutineAwareAnswer import org.mockito.kotlin.internal.KAnswer +import org.mockito.kotlin.internal.safeRunBlocking import org.mockito.stubbing.Answer import org.mockito.stubbing.OngoingStubbing @@ -92,8 +93,8 @@ fun whenever(methodCall: T): OngoingStubbing { * @return OngoingStubbing object used to stub fluently. ***Do not*** create a reference to this * returned object. */ -fun whenever(methodCall: suspend CoroutineScope.() -> T): OngoingStubbing { - return runBlocking { `when`(methodCall())!! } +fun whenever(methodCall: suspend () -> T): OngoingStubbing { + return safeRunBlocking { `when`(methodCall()) } } /** @@ -108,7 +109,7 @@ fun whenever(methodCall: suspend CoroutineScope.() -> T): OngoingStubbing */ @Deprecated("Use whenever { mock.methodCall() } instead") fun wheneverBlocking(methodCall: suspend CoroutineScope.() -> T): OngoingStubbing { - return whenever(methodCall) + return runBlocking { `when`(methodCall()) } } /** diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Stubber.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Stubber.kt index f12dc79..d090ee8 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Stubber.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Stubber.kt @@ -26,12 +26,16 @@ package org.mockito.kotlin import kotlin.reflect.KClass -import kotlinx.coroutines.runBlocking import org.mockito.Mockito -import org.mockito.internal.stubbing.answers.* +import org.mockito.internal.stubbing.answers.CallsRealMethods +import org.mockito.internal.stubbing.answers.DoesNothing +import org.mockito.internal.stubbing.answers.Returns +import org.mockito.internal.stubbing.answers.ThrowsException +import org.mockito.internal.stubbing.answers.ThrowsExceptionForClassType import org.mockito.kotlin.internal.CoroutineAwareAnswer import org.mockito.kotlin.internal.CoroutineAwareAnswer.Companion.wrapAsCoroutineAwareAnswer import org.mockito.kotlin.internal.KAnswer +import org.mockito.kotlin.internal.safeRunBlocking import org.mockito.stubbing.Answer import org.mockito.stubbing.Stubber @@ -283,7 +287,7 @@ fun Stubber.whenever(mock: T) = `when`(mock)!! * stubbed. */ fun Stubber.whenever(mock: T, methodCall: suspend T.() -> Unit) { - whenever(mock).let { runBlocking { it.methodCall() } } + whenever(mock).let { safeRunBlocking { it.methodCall() } } } /** diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Verification.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Verification.kt index 25b4667..3663254 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Verification.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Verification.kt @@ -25,10 +25,10 @@ package org.mockito.kotlin -import kotlinx.coroutines.runBlocking import org.mockito.Mockito import org.mockito.kotlin.internal.KInOrderDecorator import org.mockito.kotlin.internal.createInstance +import org.mockito.kotlin.internal.safeRunBlocking import org.mockito.verification.VerificationAfterDelay import org.mockito.verification.VerificationMode import org.mockito.verification.VerificationWithTimeout @@ -50,7 +50,7 @@ fun verify(mock: T): T { */ fun verifyBlocking(mock: T, f: suspend T.() -> Unit) { val m = Mockito.verify(mock) - runBlocking { m.f() } + safeRunBlocking { m.f() } } /** @@ -61,7 +61,7 @@ fun verifyBlocking(mock: T, f: suspend T.() -> Unit) { */ fun verifyBlocking(mock: T, mode: VerificationMode, f: suspend T.() -> Unit) { val m = Mockito.verify(mock, mode) - runBlocking { m.f() } + safeRunBlocking { m.f() } } /** diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/KInOrderDecorator.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/KInOrderDecorator.kt index 6f591f4..47b38eb 100644 --- a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/KInOrderDecorator.kt +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/KInOrderDecorator.kt @@ -25,7 +25,6 @@ package org.mockito.kotlin.internal -import kotlinx.coroutines.runBlocking import org.mockito.InOrder import org.mockito.kotlin.KInOrder import org.mockito.verification.VerificationMode @@ -33,11 +32,11 @@ import org.mockito.verification.VerificationMode class KInOrderDecorator(private val inOrder: InOrder) : KInOrder, InOrder by inOrder { override fun verifyBlocking(mock: T, f: suspend T.() -> Unit) { val m = verify(mock) - runBlocking { m.f() } + safeRunBlocking { m.f() } } override fun verifyBlocking(mock: T, mode: VerificationMode, f: suspend T.() -> Unit) { val m = verify(mock, mode) - runBlocking { m.f() } + safeRunBlocking { m.f() } } } diff --git a/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/SafeRunBlocking.kt b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/SafeRunBlocking.kt new file mode 100644 index 0000000..802e1a6 --- /dev/null +++ b/mockito-kotlin/src/main/kotlin/org/mockito/kotlin/internal/SafeRunBlocking.kt @@ -0,0 +1,90 @@ +/* + * 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 kotlin.coroutines.Continuation +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.startCoroutine +import kotlinx.coroutines.runBlocking +import org.mockito.kotlin.mock + +internal fun safeRunBlocking(block: suspend () -> T): T { + return try { + Class.forName("kotlinx.coroutines.BuildersKt") + runBlocking { block() } + } catch (_: ClassNotFoundException) { + bareBasicsRunBlocking(block) + } +} + +/** + * This function is a bare basics replacement for the proper function runBlocking from the + * kotlinx-coroutines-core library. + * + * Mockito-kotlin is compiled with kotlinx-coroutines-core library on the compileOnly classpath, so + * the library is not marked as a transitive dependency of Mockito-kotlin. + * + * Mockito-kotlin API, like [org.mockito.kotlin.whenever] accepts suspending lambda arguments to + * unify the handling of synchronous and suspending lambdas. Currently, Kotlin compiler does not yet + * allow to define overload functions, one for a synchronous lambda and one for a suspending lambda: + * it leads to resolution ambiguity issues. But once a synchronous lambda is passed into the + * Mockito-kotlin API and then flagged as potentially suspending, there is no easy way to unflag the + * suspending nature. + * + * If the project that applies Mockito-kotlin is not including the kotlinx-coroutines-core + * dependency, simply because the project does not include any suspending/coroutines functionality, + * Mockito-kotlin needs an alternative mean to execute the lambda parameter flagged as suspending. + * + * Therefor this function assumes that the lambda parameter [block], although marked suspending, + * should be considered a synchronous lambda in the absence of any coroutines infrastructure as + * delivered by the kotlinx-coroutines-core library. It takes a bare basics approach to invoke the + * lambda on the current thread without any safeguards for (potential) suspension points in the + * lambda. + * + * In future, when support for overloads just differing in suspending nature of the lambda argument + * is provided by the Kotlin compiler, this rather ugly fix should be dropped in favor of + * introducing proper overloads in the Mockito-kotlin API to cater for both synchronous and + * suspending lambdas. + */ +private fun bareBasicsRunBlocking(block: suspend () -> T): T { + val completion = SimpleContinuation() + block.startCoroutine(completion) + return completion.result!!.let { result -> + result.exceptionOrNull()?.let { throw it } + result.getOrNull()!! + } +} + +private class SimpleContinuation : Continuation { + var result: Result? = null + + override val context: CoroutineContext + get() = mock() + + override fun resumeWith(result: Result) { + this.result = result + } +} diff --git a/no-coroutine-tests/build.gradle b/no-coroutine-tests/build.gradle new file mode 100644 index 0000000..b7d0fda --- /dev/null +++ b/no-coroutine-tests/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "org.gradle.java" + id "org.jetbrains.kotlin.jvm" version "2.1.20" +} + +repositories { + mavenCentral() +} + +dependencies { + implementation "org.mockito.kotlin:mockito-kotlin" + implementation "org.jetbrains.kotlin:kotlin-stdlib" + testImplementation 'junit:junit:4.13.2' +} + +kotlin { + jvmToolchain(11) // This handles Kotlin compilation, Java compilation, and Gradle attributes automatically +} diff --git a/no-coroutine-tests/settings.gradle b/no-coroutine-tests/settings.gradle new file mode 100644 index 0000000..5378581 --- /dev/null +++ b/no-coroutine-tests/settings.gradle @@ -0,0 +1 @@ +includeBuild '..' diff --git a/no-coroutine-tests/src/test/kotlin/RegressionTest.kt b/no-coroutine-tests/src/test/kotlin/RegressionTest.kt new file mode 100644 index 0000000..709bcef --- /dev/null +++ b/no-coroutine-tests/src/test/kotlin/RegressionTest.kt @@ -0,0 +1,22 @@ +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.stubbing + +class RegressionTest { + private val a = mock() + + @Test + fun `should stub synchronous mock in absence of the 'kotlinx-coroutines-core' library`() { + stubbing(a) { + on { doSomething() } + .thenReturn("a") + } + + assertEquals("a", a.doSomething()) + } +} + +interface A { + fun doSomething(): String +} diff --git a/settings.gradle b/settings.gradle index 6ccef96..dc19b76 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,3 +6,4 @@ rootProject.name = 'mockito-kotlin-root' include 'mockito-kotlin' includeBuild 'tests' +includeBuild 'no-coroutine-tests' diff --git a/tests/build.gradle b/tests/build.gradle index ae3c444..33d1b07 100644 --- a/tests/build.gradle +++ b/tests/build.gradle @@ -1,6 +1,3 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { id "org.gradle.java" id "org.jetbrains.kotlin.jvm" version "${testKotlinVersion}"