Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]. */
Expand Down Expand Up @@ -66,7 +67,7 @@ fun <T> then(mock: T): Then<T> {
/** Alias for [Then.should], with suspending lambda. */
fun <T, R> Then<T>.shouldBlocking(f: suspend T.() -> R): R {
val m = should()
return runBlocking { m.f() }
return safeRunBlocking { m.f() }
}

/** Alias for [BDDMyOngoingStubbing.will] */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

@jselbo jselbo Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised ktfmt didn't remove the kotlinx.coroutines.runBlocking import. Let's remove it here and in OngoingStubbing.kt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runBlocking is still used directly in 2 expicit 'blocking' stub methods (e.g. givenBlocking andwheneverBlocking) that already exist for longer time. These methods have a parameter of type suspend CoroutineScope.() -> T. The CoroutineScope receiver type is preventing the use of safeRunBlocking.

I have no clear picture whether it would be feasable to remove the receiver and simplify the parameters to suspend () -> T. I suppose that could break existing usages of these stub methods. And it should not be a problem to use runBlocking directly, because these expicit 'blocking' stub methods are typically applied in the context of projects with coroutines, so the kotlinx-coroutines-core library should be available.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could extend the test suite you added with calls to these methods? Then we know for sure. For the one where it already took a coroutine as argument prior to your changes, it would be safe to use runBlocking. That said, for consistency and potential incorrect copy-pasting in the future, let's use safeRunBlocking in as many cases as we can.

Copy link
Contributor Author

@m-koops m-koops Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's already the current state since my last commit this morning.

Only 2 spots use runBlocking directly as described above.

Talking about the new test suite, I had another thought popping up to my mind: To split the 'tests' suite in two: 1 suite for all synchronous tests in a suite without dependency to kotlinx-coroutines-core library, and 1 suite for all coroutine related tests. That would safeguard the project best for regressions I guess.
I would suggest to do that split/move in a new PR of course.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that sounds like a good idea! Ack about runBlocking. Will publish this as 6.2.1.

import org.mockito.stubbing.OngoingStubbing
import org.mockito.stubbing.Stubber

Expand Down Expand Up @@ -143,7 +143,7 @@ class KStubbing<out T : Any>(val mock: T) {
fun <R : Any> onGeneric(methodCall: suspend T.() -> R?, clazz: KClass<R>): OngoingStubbing<R> {
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
Expand Down Expand Up @@ -207,7 +207,7 @@ class KStubbing<out T : Any>(val mock: T) {
*/
@Deprecated("Use on { methodCall } instead")
fun <T : Any, R> KStubbing<T>.onBlocking(methodCall: suspend T.() -> R): OngoingStubbing<R> {
return runBlocking { `when`<R>(mock.methodCall())!! }
return safeRunBlocking { `when`<R>(mock.methodCall())!! }
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -92,8 +93,8 @@ fun <T> whenever(methodCall: T): OngoingStubbing<T> {
* @return OngoingStubbing object used to stub fluently. ***Do not*** create a reference to this
* returned object.
*/
fun <T> whenever(methodCall: suspend CoroutineScope.() -> T): OngoingStubbing<T> {
return runBlocking { `when`<T>(methodCall())!! }
fun <T> whenever(methodCall: suspend () -> T): OngoingStubbing<T> {
return safeRunBlocking { `when`<T>(methodCall()) }
}

/**
Expand All @@ -108,7 +109,7 @@ fun <T> whenever(methodCall: suspend CoroutineScope.() -> T): OngoingStubbing<T>
*/
@Deprecated("Use whenever { mock.methodCall() } instead")
fun <T> wheneverBlocking(methodCall: suspend CoroutineScope.() -> T): OngoingStubbing<T> {
return whenever(methodCall)
return runBlocking { `when`(methodCall()) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why doesn't this use safeRunBlocking?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see my other response.

}

/**
Expand Down
10 changes: 7 additions & 3 deletions mockito-kotlin/src/main/kotlin/org/mockito/kotlin/Stubber.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -283,7 +287,7 @@ fun <T> Stubber.whenever(mock: T) = `when`(mock)!!
* stubbed.
*/
fun <T> Stubber.whenever(mock: T, methodCall: suspend T.() -> Unit) {
whenever(mock).let { runBlocking { it.methodCall() } }
whenever(mock).let { safeRunBlocking { it.methodCall() } }
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -50,7 +50,7 @@ fun <T> verify(mock: T): T {
*/
fun <T> verifyBlocking(mock: T, f: suspend T.() -> Unit) {
val m = Mockito.verify(mock)
runBlocking { m.f() }
safeRunBlocking { m.f() }
}

/**
Expand All @@ -61,7 +61,7 @@ fun <T> verifyBlocking(mock: T, f: suspend T.() -> Unit) {
*/
fun <T> verifyBlocking(mock: T, mode: VerificationMode, f: suspend T.() -> Unit) {
val m = Mockito.verify(mock, mode)
runBlocking { m.f() }
safeRunBlocking { m.f() }
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,18 @@

package org.mockito.kotlin.internal

import kotlinx.coroutines.runBlocking
import org.mockito.InOrder
import org.mockito.kotlin.KInOrder
import org.mockito.verification.VerificationMode

class KInOrderDecorator(private val inOrder: InOrder) : KInOrder, InOrder by inOrder {
override fun <T> verifyBlocking(mock: T, f: suspend T.() -> Unit) {
val m = verify(mock)
runBlocking { m.f() }
safeRunBlocking { m.f() }
}

override fun <T> verifyBlocking(mock: T, mode: VerificationMode, f: suspend T.() -> Unit) {
val m = verify(mock, mode)
runBlocking { m.f() }
safeRunBlocking { m.f() }
}
}
Original file line number Diff line number Diff line change
@@ -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 <T> 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 <T> bareBasicsRunBlocking(block: suspend () -> T): T {
val completion = SimpleContinuation<T>()
block.startCoroutine(completion)
return completion.result!!.let { result ->
result.exceptionOrNull()?.let { throw it }
result.getOrNull()!!
}
}

private class SimpleContinuation<T> : Continuation<T> {
var result: Result<T>? = null

override val context: CoroutineContext
get() = mock()

override fun resumeWith(result: Result<T>) {
this.result = result
}
}
18 changes: 18 additions & 0 deletions no-coroutine-tests/build.gradle
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions no-coroutine-tests/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
includeBuild '..'
22 changes: 22 additions & 0 deletions no-coroutine-tests/src/test/kotlin/RegressionTest.kt
Original file line number Diff line number Diff line change
@@ -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<A>()

@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
}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ rootProject.name = 'mockito-kotlin-root'

include 'mockito-kotlin'
includeBuild 'tests'
includeBuild 'no-coroutine-tests'
3 changes: 0 additions & 3 deletions tests/build.gradle
Original file line number Diff line number Diff line change
@@ -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}"
Expand Down