2

I'm writing the unit test of a presenter that fetches data from an API :

class SearchPresenter constructor(
    private val view: SearchContract.View,
    private val coroutineScope: CoroutineScope,
    private val dispatcherProvider: DispatcherProvider,
    private val userService: UserService
) : SearchContract.Presenter {

    override fun fetchData() {
        coroutineScope.launch(dispatcherProvider.io) {
            val response = withContext(dispatcherProvider.io) {
                userService.getUsers(1u)
            }
            if (response.isSuccessful) {
                view.displayUsers(response.body()!!.data)
            } else {
                view.displayError()
            }
        }
    }
}

I use 2 different dispatcher depending on the context (test or not):

interface DispatcherProvider {
    val main: CoroutineDispatcher
    val io: CoroutineDispatcher
    val default: CoroutineDispatcher
}

class DefaultDispatcher : DispatcherProvider {
    override val main: CoroutineDispatcher
        get() = Dispatchers.Main
    override val io: CoroutineDispatcher
        get() = Dispatchers.IO
    override val default: CoroutineDispatcher
        get() = Dispatchers.Default
}

For my test I use this Dispatcher:

class DispatcherProviderTest : DispatcherProvider {
    override val main: CoroutineDispatcher
        get() = TestCoroutineDispatcher()
    override val io: CoroutineDispatcher
        get() = TestCoroutineDispatcher()
    override val default: CoroutineDispatcher
        get() = TestCoroutineDispatcher()
}

Here is my presenter test class:

class SearchPresenterTest : CoroutineBasedTest() {
    lateinit var view: SearchContract.View
    lateinit var presenter: SearchPresenter
    lateinit var dispatcherProvider: DispatcherProvider
    lateinit var userService: UserService


    @Before
    fun setUp() {
        view = mockk()
        userService = mockk(relaxed = true)
        dispatcherProvider = testCoroutineContextProvider
        presenter = SearchPresenter(view, coroutineScope.scope, dispatcherProvider, userService)
    }


    @Test
    fun testSuccess() {
        val users = listOf<UserPreview>(
            UserPreview(
                "testId",
                "testTitle",
                "testFirstName",
                "testLastName",
                "test.jpg"
            )
        )
        coEvery {
            userService.getUsers(1u)
        } answers {
            Response.success(
                Page(
                    users, 1u
                )
            )
        }
        presenter.fetchData()
        verify { view.displayUsers(users) }
    }

This class inherits from CoroutineBasedTest:

abstract class CoroutineBasedTest {

    @get:Rule
    val coroutineScope = TestCoroutineScopeRule()

    protected val testCoroutineContextProvider = DispatcherProviderTest()


    class TestCoroutineScopeRule : TestWatcher() {

        lateinit var scope: CoroutineScope
        val mainThreadSurrogate = Executors.newSingleThreadExecutor().asCoroutineDispatcher()

        override fun starting(description: Description?) {
            super.starting(description)
            scope = CoroutineScope(Job() + mainThreadSurrogate)
        }

        override fun finished(description: Description?) {
            super.finished(description)
            scope.cancel()
        }
    }

}

I must have a problem with the scope or dispatcher not being properly cleaned. But I can't find the fix it. When I run the test, I have a successful result but with this exception:

Exception in thread "Test worker @coroutine#4" io.mockk.MockKException: no answer found for: View(#1).displayUsers([UserPreview(id=testId, title=testTitle, firstName=testFirstName, lastName=testLastName, picture=test.jpg)]) at io.mockk.impl.stub.MockKStub.defaultAnswer(MockKStub.kt:93) at io.mockk.impl.stub.MockKStub.answer(MockKStub.kt:42) at io.mockk.impl.recording.states.AnsweringState.call(AnsweringState.kt:16) at io.mockk.impl.recording.CommonCallRecorder.call(CommonCallRecorder.kt:53) at io.mockk.impl.stub.MockKStub.handleInvocation(MockKStub.kt:266) at io.mockk.impl.instantiation.JvmMockFactoryHelper$mockHandler$1.invocation(JvmMockFactoryHelper.kt:23) at io.mockk.proxy.jvm.advice.Interceptor.call(Interceptor.kt:21) at io.mockk.proxy.jvm.advice.BaseAdvice.handle(BaseAdvice.kt:42) at io.mockk.proxy.jvm.advice.jvm.JvmMockKProxyInterceptor.interceptNoSuper(JvmMockKProxyInterceptor.java:45) at com.thefork.challenge.search.SearchContract$View$Subclass0.displayUsers(Unknown Source) at com.thefork.challenge.search.SearchPresenter$fetchData$1.invokeSuspend(SearchPresenter.kt:24) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106) at kotlinx.coroutines.test.TestCoroutineDispatcher.dispatch(TestCoroutineDispatcher.kt:45) at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:322) at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:30) at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable$default(Cancellable.kt:25) at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:110) at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:126) at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:56) at kotlinx.coroutines.BuildersKt.launch(Unknown Source) at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:47) at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source) at com.thefork.challenge.search.SearchPresenter.fetchData(SearchPresenter.kt:19) at com.thefork.challenge.search.SearchPresenterTest.testSuccess(SearchPresenterTest.kt:53) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26) at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27) at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:61) at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) at org.junit.runners.ParentRunner.run(ParentRunner.java:413) at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110) at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58) at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38) at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62) at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24) at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94) at com.sun.proxy.$Proxy2.processTestClass(Unknown Source) at org.gradle.api.internal.tasks.testing.worker.TestWorker.processTestClass(TestWorker.java:121) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24) at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:182) at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:164) at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:414) at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64) at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56) at java.base/java.lang.Thread.run(Thread.java:829) Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [CoroutineId(4), "coroutine#4":StandaloneCoroutine{Cancelling}@34af8dd6, TestCoroutineDispatcher[scheduler=kotlinx.coroutines.test.TestCoroutineScheduler@6637b783]] BUILD SUCCESSFUL in 9s

2
  • You just need to write something like every { view.displayUsers(users) } just runs or add relaxed=true. Your call is not mocked, but the test completes successfully - the method is called. Commented Aug 2, 2022 at 19:55
  • My test complete successfully already. However, I still have this exception which is triggered during the test. I tried making the changes you mentioned but to no result. (I may have misunderstood) Commented Aug 3, 2022 at 15:26

1 Answer 1

0

Have you tried adding an "answer" for the call in question?

coEvery { 
    view.displayUsers(users)
} answers { 
    // some response
}

That might get that error log to go away.

As for the test passing even though there is an error, this can happen often when the exception itself happens in a background coroutine that does not run on the current thread. This is because in a test, the "default uncaught thread exception handler" for background threads just logs an error. You could try wrapping the test in something like

dispatcherProvider.io.runBlockingTest {
   ...
}

to see if that helps surface the error.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.