19

I'm new at testing, trying to take second flow value and assert it, When i run this test one by one runs fine but when i run whole test once first test runs fine and rest of test give me timeout error.

Error :

After waiting for 60000 ms, the test coroutine is not completing
kotlinx.coroutines.test.UncompletedCoroutinesError: After waiting for 60000 ms, the test coroutine is not completing
    at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine$3$3.invokeSuspend(TestBuilders.kt:304)
    (Coroutine boundary)
@OptIn(ExperimentalCoroutinesApi::class)
class HomeViewModelTest {

    private lateinit var viewModel: HomeViewModel
    private val testDispatcher = UnconfinedTestDispatcher()

    @Before
    fun setup() {
        viewModel = HomeViewModel(FakeOrderRepository())
        Dispatchers.setMain(testDispatcher)
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
        testDispatcher.cancel()
    }

    @Test
    fun flowViewModelTesting1() = runTest {
        val result = viewModel.homeUiState.drop(1).first()
        assertThat(true).isTrue()
    }


    @Test
    fun flowViewModelTesting2() = runTest {
        val result = viewModel.homeUiState.drop(1).first()
        assertThat(true).isTrue()
    }
}
Jonas
  • 207
  • 2
  • 7
  • How do you update the value of `homeUiState`? Are you sure it is updated? – broot Jun 05 '22 at 11:31
  • It looks like `homeUiState` is always updated only once per test session, so only the first test finishes. Do you share some state/objects between instances of `HomeViewModel` that might cause `homeUiState` to be updated only once, even when multiple `HomeViewModel` instances are created? – broot Jun 05 '22 at 12:07

8 Answers8

7

I had the same issue. Replacing UnconfinedTestDispatcher() with StandardTestDispatcher() will solve the problem.

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
Ying Ye
  • 71
  • 1
  • 3
1

Most of the time reason for this error is that you have not finish your observer variable call.

At the end of your test case call finish() method on your observer variable.

observerVariable.finish()
Rajneesh Shukla
  • 1,048
  • 13
  • 21
1
@RunWith(AndroidJUnit4::class)
class Test {

    private val dispatcher = TestCoroutineDispatcher()
    private val testScope = TestCoroutineScope(dispatcher)
    
    @Before
    fun setUp() {
        Dispatchers.setMain(dispatcher)
    }
    @After
    fun tearDown() {
        Dispatchers.resetMain()
        dispatcher.cleanupTestCoroutines()
        unmockkAll()
    }
    
    @OptIn(ExperimentalCoroutinesApi::class)
    @Test
    fun test() = runTest {
        testScope.launch {
                 // call your suspend method here
                Utils.testSuspendMethod()
            )
          //add here assertions for the method
        }

    }

}
0

You're not using the testDispatcher. You should pass it to runTest like:

runTest(UnconfinedTestDispatcher()) {}

https://developer.android.com/kotlin/coroutines/test

0

This is maybe because there is a coroutine that never ends in your view model or in his dependencies (like the repository). There is a couple of things you can try:

  1. I am not sure if coroutines debugger is available on Android Studio right now, but if it is you can put a breakpoint after all your test functions and use it to check which coroutine is still running.

  2. Check in your view model and its dependencies if you are using an external scope like CoroutineScope(SupervisorJob() + Dispatchers.Default). If you are, make sure you are using with a suspend function that finishes in 60s. If you are using to observe a value like in myFlow.onEach{}.launchIn(externalScope) it may be that because the job is still running.

  3. Don't replace the dispatchers of your external scope with the test scope (like in CoroutineScope(SupervisorJob() + myTestDispatcher)), otherwise the test execution will block the execution of your suspend functions and the suspend function will only return after your test is over, which may cause this infinite testing behavior or a screen being stucked if you are making instrumented tests. Also, put your scopes and dispatchers on the constructor, so in tests you can control which one is being used.

    @Test
    fun flowViewModelTesting1() = runTest {

        /** Given **/
        val viewModel = HomeViewModel(
            orderRepository = FakeOrderRepository(
                // ioDispatcher may execute execute the task of call the server
                // or in the fake version wait 1,000 ms and return a mock
                ioDispatcher =  TestDispatcher(this.testScheduler),
                // External scope may launch a order update
                externalScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
            )
        )

        /** When **/
        val result = viewModel.homeUiState.drop(1).first()

        /** Then **/
        assertEquals(
            expected = true,
            actual = result.myBoolean
        )
    }
Allan Veloso
  • 5,823
  • 1
  • 38
  • 36
-1

All of these answers are wrong, you are looking for dispatchTimeoutMs:

@Test
fun `my long-running test`() = runTest(
  dispatchTimeoutMs = 60000L, // Increase this number as needed, this is the default which is 60 seconds in milliseconds
  context = testDispatcher // also, still pass in a TestDispatcher
) {
  // ... Rest of your long-running test
}

Normally, you should try to make your test run faster, or test smaller things, but I have needed to do this from time-to-time.

Mike D.
  • 9
  • 1
-2

Actually, the way I handled it:

I was trying to read state from my viewModel, and I realized that I needed to cancel the job reading the state:

@Test
fun `test click gets right function call if token timed out`() =
    runTest(StandardTestDispatcher()) {
        var viewState: ChangeBranchViewState
        coEvery { changeBranchAppointmentsUseCase.invoke(any()) } returns
                GetAppointmentDataResult.TokenExpired()
        viewModel.viewState.update { it.copy(branchNumberInput = "1234") }
        viewModel.onSubmit()
        assertFalse(viewModel.navigateToSettings.value)

        val job = launch {
            viewModel.viewState.collect {
                viewState = it
                assertTrue(viewState.tokenTimedOutGoToLogin)
            }
        }
        job.cancel()
    }
Kristy Welsh
  • 7,828
  • 12
  • 64
  • 106
-3

When you use runTest, so you're using StandardTestDispatcher by default that means it won't immediately run. You have to launch your test from the same dispatcher that you have been using for a ViewModel

Replace

runTest {

with

testDispather.runTest {

it should work, give it a try. I can't share much without checking your viewmodel code.

sam_k
  • 5,983
  • 14
  • 76
  • 110