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
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
.