Skip to content
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

Testing Android ViewModels that use viewModelScope + Molecule may affect other tests #121

Open
mhernand40 opened this issue Sep 22, 2022 Discussed in #118 · 0 comments
Open

Comments

@mhernand40
Copy link
Contributor

Discussed in #118

Originally posted by mhernand40 September 18, 2022
Been playing around with Molecule in my team's Android project by trying to introduce it as an implementation detail of two View Models; one that extends Jetpack's ViewModel and uses viewModelScope, and another that does not extend Jetpack's ViewModel and accepts any CoroutineScope via the constructor.

When it came to running the tests for each View Model, the tests passed when each test class was run in isolation. However, when running all the tests in one run, the test class for the View Model that extends Jetpack's ViewModel runs before the test class for the View Model that is a plain class, causing the latter's tests to fail.

It is worth noting the following:

  • The tests use runTest { … } from the Coroutines Test library with the default StandardTestDispatcher,
  • viewModelScope is used for the Jetpack ViewModel
    • Requires Dispatchers.setMain(…)/Dispatchers.resetMain()
    • the tests do not explicitly clear the ViewModel
    • the tests do not explicitly cancel viewModelScope
  • TestScope(testScheduler) is used for the View Model that does not extend Jetpack's ViewModel
    • No usage of Dispatchers.setMain(…)/Dispatchers.resetMain()

I have reduced the repro down to the following test class (no Jetpack ViewModel required):

internal class Repro {

  // This test passes but causes the next test to fail.
  @Test
  fun test1() {
    try {
      Dispatchers.setMain(StandardTestDispatcher())
      runTest {
        val moleculeScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        doRunTest(moleculeScope)
        // moleculeScope.cancel() // Uncommenting this fixes the test2 failure.
      }
    } finally {
      Dispatchers.resetMain()
    }
  }

  // This test only passes when run by itself or if it runs before test1.
  // If you rename this to test0 so that it runs before test1, it will pass.
  @Test
  fun test2() = runTest {
    val moleculeScope = TestScope(testScheduler)
    doRunTest(moleculeScope)
  }

  private fun TestScope.doRunTest(moleculeScope: CoroutineScope) {
    val event = MutableSharedFlow<String>(extraBufferCapacity = 1)
    val state = moleculeScope.launchMolecule(RecompositionClock.Immediate) {
      var value by remember { mutableStateOf("") }
      LaunchedEffect(event) { event.collect { value = it } }
      value
    }
    runCurrent()
    event.tryEmit("test")
    runCurrent()
    assertEquals("test", state.value)
  }
}

test1 simulates the scenario when testing a Jetpack ViewModel that uses viewModelScope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant