Search code examples
android-studiounit-testingkotlinandroid-jetpack-composekotlin-coroutines

How to test kotlin coroutines that return Exception?


i am writing unit test for some usecase, but i found that after i add withContext(IO) in my repository and than i run my unit test again, everything works fine except the test that expect to be fail and return exception, I've read several sites about testing coroutines but I found nothing to solve it, maybe I missed something? this is my repository

class HaditsRepository @Inject constructor(
    private val remote: IRemoteHaditsDatasource,
    @IoDispatcher private val dispatcher: CoroutineDispatcher
): IHaditsRepository {
    override fun getCategories(): Flow<ResultOf<GetCategoriesResponse>> {
        return flow {
            emit(ResultOf.Loading())
            val result = withContext(dispatcher){ remote.getCategories() }
            emit(ResultOf.Success(result))
        }.catch { e->
            emit(ResultOf.Failure(e))
        }
    }
}

this is my usecase

class GetCategoriesUsecase @Inject constructor(
   private val repository: IHaditsRepository
) {
   operator fun invoke() = repository.getCategories()
}

and this is my unit test

@OptIn(ExperimentalCoroutinesApi::class)
class GetCategoriesUsecaseTest {
    // Mock Initialize
    private lateinit var openMock: AutoCloseable

    // Coroutines
    private val testDispatcher = StandardTestDispatcher()

    // System Under Test
    private lateinit var sut: GetCategoriesUsecase

    // Given Conditions
    private suspend fun givenFailure(){
        whenever(mockDSR.getCategories()).thenThrow(dummyException)
    }
    
    // Setup
    @Before
    fun setup(){
        openMock = MockitoAnnotations.openMocks(this)
        Dispatchers.setMain(testDispatcher)
        val repository = HaditsRepository(mockDSR, testDispatcher)
        sut = GetCategoriesUsecase(repository)
    }

    @After
    fun close(){
        openMock.close()
        Dispatchers.resetMain()
    }

    ....
    // FAIL HEREEEEE
    @Test
    fun `(-) Given failure - When SUT is called - Then assert result has correct Response`(){
        runTest {
            // Given
            givenFailure()
            // When
            val result = sut().last() as ResultOf.Failure
            // Then
            Assert.assertEquals(dummyException, result.exception)
        }
    }
}

and this is what I got after I ran the test

expected: java.lang.RuntimeException<java.lang.RuntimeException> but was: java.lang.RuntimeException<java.lang.RuntimeException>
java.lang.AssertionError: expected: java.lang.RuntimeException<java.lang.RuntimeException> but was: java.lang.RuntimeException<java.lang.RuntimeException>
    at org.junit.Assert.fail(Assert.java:89)
    at org.junit.Assert.failNotEquals(Assert.java:835)
    at org.junit.Assert.assertEquals(Assert.java:120)
    at org.junit.Assert.assertEquals(Assert.java:146)
    at id.co.haditsku.usecase.GetCategoriesUsecaseTest$(-) Given failure - When SUT is called - Then assert result has correct Response$1.invokeSuspend(GetCategoriesUsecaseTest.kt:130)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
    at kotlinx.coroutines.test.TestDispatcher.processEvent$kotlinx_coroutines_test(TestDispatcher.kt:28)
    at kotlinx.coroutines.test.TestCoroutineScheduler.tryRunNextTaskUnless$kotlinx_coroutines_test(TestCoroutineScheduler.kt:100)
    at kotlinx.coroutines.test.TestCoroutineScheduler.advanceUntilIdleOr$kotlinx_coroutines_test(TestCoroutineScheduler.kt:120)
    at kotlinx.coroutines.test.TestCoroutineScheduler.advanceUntilIdle(TestCoroutineScheduler.kt:113)
    at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTestCoroutine(TestBuilders.kt:237)
    at kotlinx.coroutines.test.TestBuildersKt.runTestCoroutine(Unknown Source)
    at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invokeSuspend(TestBuilders.kt:167)
    at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invoke(TestBuilders.kt)
    at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invoke(TestBuilders.kt)
    at kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult$1.invokeSuspend(TestBuildersJvm.kt:13)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:284)
    at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
    at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
    at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
    at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
    at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
    at kotlinx.coroutines.test.TestBuildersJvmKt.createTestResult(TestBuildersJvm.kt:12)
    at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest(TestBuilders.kt:166)
    at kotlinx.coroutines.test.TestBuildersKt.runTest(Unknown Source)
    at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest(TestBuilders.kt:154)
    at kotlinx.coroutines.test.TestBuildersKt.runTest(Unknown Source)
    at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest$default(TestBuilders.kt:147)
    at kotlinx.coroutines.test.TestBuildersKt.runTest$default(Unknown Source)
    at id.co.haditsku.usecase.GetCategoriesUsecaseTest.(-) Given failure - When SUT is called - Then assert result has correct Response(GetCategoriesUsecaseTest.kt:124)
    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.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$2.run(TestWorker.java:176)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
    at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
    at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
    at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)

pleaseee hellppp


Solution

  • There is a strange issue where equality checks of Exceptions fail across coroutine boundaries even though they should be the same. It seems to be related to Exceptions being recreated using reflection and is discussed in detail here. It is something that has been dealt with in the tests of libraries like Turbine. There they use a custom exception that avoids this problem like the following:

    /**
     * This type prevents coroutines from breaking referential equality by
     * reflectively creating new instances.
     */
    internal class CustomRuntimeException(
      message: String?,
      override val cause: Throwable? = null,
    ) : RuntimeException(message)
    

    I've successfully used a similar approach. So this is what you'll want to use for your dummyException.