New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating a mock with coAnswers that contain certain suspending functions causes tests to hang #171

Closed
TAGC opened this Issue Nov 9, 2018 · 8 comments

Comments

2 participants
@TAGC

TAGC commented Nov 9, 2018

Prerequisites

Please answer the following questions for yourself before submitting an issue.

  • I am running the latest version
  • I checked the documentation and found no answer
  • I checked to make sure that this issue has not already been filed

Expected Behavior

Given a unit test where I set up a mock like this:

val executionCompletionSource = CompletableDeferred<Nothing>()
suspend fun task(): Unit = executionCompletionSource.await()
val mock = mockk<Executable> { coEvery { execute() } coAnswers { task() } }
// ...
val execution = launch { mock.execute() } 
executionCompletionSource.cancel()

I expect that the unit test eventually completes.

Current Behavior

A unit test with this code ends up hanging indefinitely, even though an analogous non-mock version of this procedure does not hang.

Failure Information (for bugs)

Steps to Reproduce

  1. Create a Gradle project with these dependencies:

    implementation(kotlin("stdlib-jdk8"))
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0")
    implementation("io.mockk:mockk:1.8.10.kotlin13")
  2. Create a file within the project with this content:

    package experiment
    
    import io.mockk.coEvery
    import io.mockk.mockk
    import kotlinx.coroutines.CompletableDeferred
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.runBlocking
    import kotlin.system.measureTimeMillis
    
    interface Executable {
        suspend fun execute()
    }
    
    fun main() {
        val executionCompletionSource = CompletableDeferred<Nothing>()
        suspend fun task(): Unit = executionCompletionSource.await()
        val mock = mockk<Executable> { coEvery { execute() } coAnswers { task() } }
    
        runBlocking {
            val execution = launch { mock.execute() } // This blocks the test indefinitely
            //val execution = launch { task() } // This works fine (~110-120 ms)
            measureTimeMillis {
                delay(100)
                executionCompletionSource.cancel()
                execution.join()
            }.also { elapsed -> println("Elapsed in $elapsed ms") }
        }
    }
  3. Run that code, verify that it hangs indefinitely.

  4. Comment out the val execution = launch { mock.execute() } line and uncomment the line beneath it

  5. Try running the code again, verifying that it no longer hangs.

Context

  • MockK version: 1.8.10.kotlin13
  • OS: Windows 10 (64-bit)
  • Kotlin version: 1.3.0
  • JDK version: 1.8.0_191
  • JUnit version: 5
  • Type of test: unit test
@TAGC

This comment has been minimized.

@oleksiyp

This comment has been minimized.

Collaborator

oleksiyp commented Nov 9, 2018

Simplest workaround is:

val execution = launch(start = CoroutineStart.LAZY) { mock.execute() } // This blocks the test indefinitely
@oleksiyp

This comment has been minimized.

Collaborator

oleksiyp commented Nov 9, 2018

The problem is that coroutines a lot more complex than it seems on a first sight.
When I was writing the code for coroutines I didn't know all that stuff under the hood.
MockK just catches calls and inside do runBlocking

The problem here is that by default launch doesn't spawn anything.
It just executes all in same execution thread.
When suspension occurs callback will be resumed on other thread.

MockK catches the call to execute and inside just do runBlocking, so the main thread is not suspending.

MockK should be transparent layer for such calls. This will require some investigation and rework.

@TAGC

This comment has been minimized.

TAGC commented Nov 9, 2018

I can confirm that works. Interesting, if you replace the delay(100) line with withTimeoutOrNull(100) { execution.join() } shouldBe null, it hangs indefinitely again.

@TAGC

This comment has been minimized.

TAGC commented Nov 9, 2018

Launching with CoroutineStart.LAZY isn't a suitable workaround for my case anyway, as it causes my test to pass even when it should fail. I'll create a handwritten mock instead for the time being.

oleksiyp added a commit that referenced this issue Nov 10, 2018

oleksiyp added a commit that referenced this issue Nov 10, 2018

oleksiyp added a commit that referenced this issue Nov 10, 2018

oleksiyp added a commit that referenced this issue Nov 10, 2018

@oleksiyp

This comment has been minimized.

Collaborator

oleksiyp commented Nov 14, 2018

New release with fixes is out v1.8.13 or v1.8.13.kotlin13. Please verify

@TAGC

This comment has been minimized.

TAGC commented Nov 16, 2018

Yep, with v1.8.13 the test passes when it should and fails when it should. Cheers 👍

@TAGC TAGC closed this Nov 16, 2018

@oleksiyp

This comment has been minimized.

Collaborator

oleksiyp commented Nov 16, 2018

Great. Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment