Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Untangling Coroutine Testing (Droidcon Berlin 2022)

Untangling Coroutine Testing (Droidcon Berlin 2022)

Coroutines are embraced on Android as a tool to perform asynchronous operations and manage threading in your apps. Testing them requires some extra work and a solid understanding of scopes and dispatchers. In this talk, we’ll look at how to test coroutines with the latest available testing APIs introduced in kotlinx.coroutines 1.6, from the simplest cases all the way to Flows.

For more, see
https://zsmb.co/talks/untangling-coroutine-testing/

Márton Braun

July 07, 2022
Tweet

More Decks by Márton Braun

Other Decks in Programming

Transcript

  1. Agenda • Testing suspending functions • The coroutine test APIs

    • Best practices • Handling the Main dispatcher • Flows & StateFlows
  2. kotlinx.coroutines.test 1.6+ APIs runTest TestDispatcher TestScope TestCoroutineScheduler Pre-1.6 APIs runBlockingTest

    TestCoroutineDispatcher TestCoroutineScope pauseDispatcher resumeDispatcher @Experimental
  3. kotlinx.coroutines.test 1.6+ APIs runTest TestDispatcher TestScope TestCoroutineScheduler Pre-1.6 APIs runBlockingTest

    TestCoroutineDispatcher TestCoroutineScope pauseDispatcher resumeDispatcher @Experimental @Deprecated
  4. kotlinx.coroutines.test 1.6+ APIs runTest TestDispatcher TestScope TestCoroutineScheduler Pre-1.6 APIs runBlockingTest

    TestCoroutineDispatcher TestCoroutineScope pauseDispatcher resumeDispatcher @Experimental @Deprecated @Experimental
  5. Testing suspending functions suspend fun fetchData(): String { delay(1000L) return

    "Hello world" } @Test fun dataIsHelloWorld() { val data = fetchData() assertEquals("Hello world", data) }
  6. Testing suspending functions suspend fun fetchData(): String { delay(1000L) return

    "Hello world" } @Test fun dataIsHelloWorld() { val data = fetchData() assertEquals("Hello world", data) }
  7. Testing suspending functions suspend fun fetchData(): String { delay(1000L) return

    "Hello world" } @Test fun dataIsHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) } import kotlinx.coroutines.test.runTest
  8. Testing suspending functions suspend fun fetchData(): String { delay(1000L) return

    "Hello world" } @Test fun dataIsHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) } import kotlinx.coroutines.test.runTest
  9. Testing suspending functions suspend fun fetchData(): String { delay(1000L) return

    "Hello world" } @Test fun dataIsHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) } import kotlinx.coroutines.test.runTest
  10. Testing suspending functions suspend fun fetchData(): String { delay(1000L) return

    "Hello world" } @Test fun dataIsHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) } import kotlinx.coroutines.test.runTest
  11. Testing suspending functions suspend fun fetchData(): String { delay(1000L) return

    "Hello world" } @Test fun dataIsHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) } import kotlinx.coroutines.test.runTest
  12. Testing Dispatcher changes suspend fun fetchData(): String = withContext(Dispatchers.IO) {

    delay(1000L) "Hello world" } @Test fun dataIsHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) } import kotlinx.coroutines.test.runTest
  13. Testing Dispatcher changes suspend fun fetchData(): String = withContext(Dispatchers.IO) {

    delay(1000L) "Hello world" } @Test fun dataIsHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) } import kotlinx.coroutines.test.runTest *
  14. Testing Dispatcher changes suspend fun fetchData(): String = withContext(Dispatchers.IO) {

    delay(1000L) "Hello world" } @Test fun dataIsHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) } Test thread Dispatchers.IO
  15. Testing Dispatcher changes suspend fun fetchData(): String = withContext(Dispatchers.IO) {

    delay(1000L) "Hello world" } @Test fun dataIsHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) } Test thread Dispatchers.IO fetchData()
  16. Testing Dispatcher changes suspend fun fetchData(): String = withContext(Dispatchers.IO) {

    delay(1000L) "Hello world" } @Test fun dataIsHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) } Test thread Dispatchers.IO fetchData()
  17. Testing Dispatcher changes suspend fun fetchData(): String = withContext(Dispatchers.IO) {

    delay(1000L) "Hello world" } @Test fun dataIsHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) } Test thread Dispatchers.IO fetchData() delay()
  18. Testing Dispatcher changes suspend fun fetchData(): String = withContext(Dispatchers.IO) {

    delay(1000L) "Hello world" } @Test fun dataIsHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) } Test thread Dispatchers.IO fetchData() delay()
  19. Testing Dispatcher changes suspend fun fetchData(): String = withContext(Dispatchers.IO) {

    delay(1000L) "Hello world" } @Test fun dataIsHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) } Test thread Dispatchers.IO fetchData() delay() assert()
  20. Testing Dispatcher changes suspend fun fetchData(): String = withContext(Dispatchers.IO) {

    delay(1000L) "Hello world" } @Test fun dataIsHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) } Test thread Dispatchers.IO fetchData() delay() assert()
  21. Testing new coroutines @Test fun directExample() = runTest { val

    repo = UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals( listOf("Alice", "Bob"), repo.getAllUsers() ) }
  22. Testing new coroutines @Test fun directExample() = runTest { val

    repo = UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals( listOf("Alice", "Bob"), repo.getAllUsers() ) }
  23. Testing new coroutines class ProfileViewModel : ViewModel() { lateinit var

    user: User fun initialize() { viewModelScope.launch { user = fetchUser() } } } @Test fun directExample() = runTest { val repo = UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals( listOf("Alice", "Bob"), repo.getAllUsers() ) }
  24. Testing new coroutines class ProfileViewModel : ViewModel() { lateinit var

    user: User fun initialize() { viewModelScope.launch { user = fetchUser() } } } @Test fun indirectExample() = runTest { val viewModel = ProfileViewModel() viewModel.initialize() assertEquals("Sam", viewModel.user.name) } @Test fun directExample() = runTest { val repo = UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals( listOf("Alice", "Bob"), repo.getAllUsers() ) }
  25. Testing new coroutines class ProfileViewModel : ViewModel() { lateinit var

    user: User fun initialize() { viewModelScope.launch { user = fetchUser() } } } @Test fun indirectExample() = runTest { val viewModel = ProfileViewModel() viewModel.initialize() assertEquals("Sam", viewModel.user.name) } @Test fun directExample() = runTest { val repo = UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals( listOf("Alice", "Bob"), repo.getAllUsers() ) }
  26. Testing new coroutines class ProfileViewModel : ViewModel() { lateinit var

    user: User fun initialize() { viewModelScope.launch { user = fetchUser() } } } @Test fun indirectExample() = runTest { val viewModel = ProfileViewModel() viewModel.initialize() assertEquals("Sam", viewModel.user.name) } @Test fun directExample() = runTest { val repo = UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals( listOf("Alice", "Bob"), repo.getAllUsers() ) }
  27. • Queues up coroutines on the scheduler • You need

    to advance those coroutines manually StandardTestDispatcher
  28. • Queues up coroutines on the scheduler • You need

    to advance those coroutines manually • runTest uses a StandardTestDispatcher by default StandardTestDispatcher runTest StandardTestDispatcher TestScope
  29. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) }
  30. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } this: TestScope
  31. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) }
  32. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } UserRepo()
  33. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } UserRepo() reg("Alice")
  34. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } UserRepo() reg("Bob") reg("Alice")
  35. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } UserRepo() assert() reg("Bob") reg("Alice")
  36. StandardTestDispatcher UserRepo() assert() @Test fun standardTest() = runTest { val

    repo = UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } reg("Bob") reg("Alice")
  37. • You need to advance those coroutines manually StandardTestDispatcher •

    runCurrent() • advanceTimeBy(delayTimeMillis: Long) 80 ms 90 ms 100 ms 500 ms 750 ms
  38. • You need to advance those coroutines manually StandardTestDispatcher 80

    ms 90 ms 100 ms 500 ms 750 ms • runCurrent() • advanceTimeBy(delayTimeMillis = 100)
  39. • You need to advance those coroutines manually StandardTestDispatcher •

    runCurrent() • advanceTimeBy(delayTimeMillis = 100) 80 ms 90 ms 100 ms 500 ms 750 ms
  40. • You need to advance those coroutines manually StandardTestDispatcher •

    runCurrent() • advanceTimeBy(delayTimeMillis: Long) • advanceUntilIdle() 80 ms 90 ms 100 ms 500 ms 750 ms
  41. • You need to advance those coroutines manually StandardTestDispatcher •

    runCurrent() • advanceTimeBy(delayTimeMillis: Long) • advanceUntilIdle() 80 ms 90 ms 100 ms 500 ms 750 ms
  42. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) }
  43. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } advanceUntilIdle() assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) }
  44. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } advanceUntilIdle() assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) }
  45. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } advanceUntilIdle() assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } UserRepo()
  46. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } advanceUntilIdle() assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } UserRepo() reg("Alice")
  47. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } advanceUntilIdle() assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } UserRepo() reg("Bob") reg("Alice")
  48. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } advanceUntilIdle() assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } UserRepo() reg("Alice") reg("Bob")
  49. Idle StandardTestDispatcher @Test fun standardTest() = runTest { val repo

    = UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } advanceUntilIdle() assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } UserRepo() reg("Alice") reg("Bob")
  50. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } advanceUntilIdle() assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } UserRepo() reg("Alice") reg("Bob") Idle
  51. StandardTestDispatcher UserRepo() reg("Alice") reg("Bob") @Test fun standardTest() = runTest {

    val repo = UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } advanceUntilIdle() assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) }
  52. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } advanceUntilIdle() assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } UserRepo() reg("Alice") reg("Bob") assert()
  53. StandardTestDispatcher @Test fun standardTest() = runTest { val repo =

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } advanceUntilIdle() assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } UserRepo() reg("Alice") reg("Bob") assert()
  54. UnconfinedTestDispatcher • Starts new coroutines eagerly (like runBlockingTest) • Can

    be a good choice for simple tests • Does not emulate real concurrency
  55. UnconfinedTestDispatcher @Test fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) { val userRepo =

    UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) }
  56. UnconfinedTestDispatcher @Test fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) { val userRepo =

    UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) }
  57. UnconfinedTestDispatcher @Test fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) { val userRepo =

    UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) } this: TestScope
  58. UnconfinedTestDispatcher @Test fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) { val userRepo =

    UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) } UserRepo()
  59. UnconfinedTestDispatcher @Test fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) { val userRepo =

    UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) } UserRepo() reg("Alice")
  60. UnconfinedTestDispatcher @Test fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) { val userRepo =

    UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) } UserRepo() reg("Alice") reg("Bob")
  61. UnconfinedTestDispatcher @Test fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) { val userRepo =

    UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) } UserRepo() reg("Alice") reg("Bob") assert()
  62. UnconfinedTestDispatcher @Test fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) { val userRepo =

    UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) } UserRepo() reg("Alice") reg("Bob") assert()
  63. TestDispatchers Queues up new coroutines Use by default Starts new

    coroutines eagerly Use selectively • While migrating tests from old APIs • As the Main dispatcher • For coroutines that collect values StandardTestDispatcher UnconfinedTestDispatcher
  64. Injecting dispatchers class Repository(private val database: Database) { private val

    scope = CoroutineScope(Dispatchers.IO) fun initialize() { scope.launch { database.populate() } } suspend fun fetchData(): String = withContext(Dispatchers.IO) { database.read() } }
  65. Injecting dispatchers class Repository(private val database: Database) { private val

    scope = CoroutineScope(Dispatchers.IO) fun initialize() { scope.launch { database.populate() } } suspend fun fetchData(): String = withContext(Dispatchers.IO) { database.read() } }
  66. Injecting dispatchers class Repository(private val database: Database) { private val

    scope = CoroutineScope(Dispatchers.IO) fun initialize() { scope.launch { database.populate() } } suspend fun fetchData(): String = withContext(Dispatchers.IO) { database.read() } }
  67. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository(FakeDatabase()) repository.initialize() val data = repository.fetchData() assertEquals("Hello world", data) }
  68. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository(FakeDatabase()) repository.initialize() val data = repository.fetchData() assertEquals("Hello world", data) } Test thread Dispatchers.IO
  69. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository(FakeDatabase()) repository.initialize() val data = repository.fetchData() assertEquals("Hello world", data) } Repo(FakeDb()) Test thread Dispatchers.IO
  70. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository(FakeDatabase()) repository.initialize() val data = repository.fetchData() assertEquals("Hello world", data) } Repo(FakeDb()) Test thread Dispatchers.IO db.populate() fun initialize() { scope.launch { database.populate() } }
  71. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository(FakeDatabase()) repository.initialize() val data = repository.fetchData() assertEquals("Hello world", data) } Repo(FakeDb()) Test thread Dispatchers.IO db.read() db.populate() suspend fun fetchData() = withContext(Dispatchers.IO) { database.read() }
  72. assert() Injecting dispatchers @Test fun repoTest() = runTest { val

    repository = Repository(FakeDatabase()) repository.initialize() val data = repository.fetchData() assertEquals("Hello world", data) } Repo(FakeDb()) Test thread Dispatchers.IO db.read() db.populate()
  73. assert() Injecting dispatchers Repo(FakeDb()) Test thread Dispatchers.IO db.read() db.populate() @Test

    fun repoTest() = runTest { val repository = Repository(FakeDatabase()) repository.initialize() val data = repository.fetchData() assertEquals("Hello world", data) }
  74. Injecting dispatchers Repo(FakeDb()) Test thread Dispatchers.IO db.read() db.populate() assert() @Test

    fun repoTest() = runTest { val repository = Repository(FakeDatabase()) repository.initialize() val data = repository.fetchData() assertEquals("Hello world", data) }
  75. Injecting dispatchers Repo(FakeDb()) Test thread Dispatchers.IO db.read() db.populate() assert() @Test

    fun repoTest() = runTest { val repository = Repository(FakeDatabase()) repository.initialize() val data = repository.fetchData() assertEquals("Hello world", data) }
  76. , ioDispatcher ioDispatcher Injecting dispatchers class Repository( Dispatchers.IO) fun initialize()

    { scope.launch { database.populate() } } suspend fun fetchData(): String = withContext( ) { database.read() } } private val database: Database) { private val scope = CoroutineScope( Dispatchers.IO
  77. Dispatchers.IO Injecting dispatchers class Repository( private val ioDispatcher: CoroutineDispatcher =

    Dispatchers.IO, ioDispatcher) fun initialize() { scope.launch { database.populate() } } suspend fun fetchData(): String = withContext( ) { database.read() } } private val database: Database ) { private val scope = CoroutineScope( ioDispatcher ,
  78. Dispatchers.IO Injecting dispatchers class Repository( private val ioDispatcher: CoroutineDispatcher =

    Dispatchers.IO, ioDispatcher) fun initialize() { scope.launch { database.populate() } } suspend fun fetchData(): String = withContext( ) { database.read() } } private val database: Database ) { private val scope = CoroutineScope( ioDispatcher ,
  79. FakeDatabase(), @Test fun repoTest() = runTest { val repository =

    Repository( ) repository.initialize() val data = repository.fetchData() assertEquals("Hello world", data) } Injecting dispatchers
  80. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository( database = , ioDispatcher = StandardTestDispatcher(testScheduler), ) repository.initialize() advanceUntilIdle() val data = repository.fetchData() assertEquals("Hello world", data) } FakeDatabase()
  81. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository( database = FakeDatabase(), ioDispatcher = StandardTestDispatcher(testScheduler), ) repository.initialize() advanceUntilIdle() val data = repository.fetchData() assertEquals("Hello world", data) } this: TestScope
  82. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository( database = FakeDatabase(), ioDispatcher = StandardTestDispatcher(testScheduler), ) repository.initialize() advanceUntilIdle() val data = repository.fetchData() assertEquals("Hello world", data) }
  83. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository( database = FakeDatabase(), ioDispatcher = StandardTestDispatcher(testScheduler), ) repository.initialize() advanceUntilIdle() val data = repository.fetchData() assertEquals("Hello world", data) } Repo(...)
  84. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository( database = FakeDatabase(), ioDispatcher = StandardTestDispatcher(testScheduler), ) repository.initialize() advanceUntilIdle() val data = repository.fetchData() assertEquals("Hello world", data) } fun initialize() { scope.launch { database.populate() } } Repo(...)
  85. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository( database = FakeDatabase(), ioDispatcher = StandardTestDispatcher(testScheduler), ) repository.initialize() advanceUntilIdle() val data = repository.fetchData() assertEquals("Hello world", data) } Repo(...) db.populate() fun initialize() { scope.launch { database.populate() } }
  86. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository( database = FakeDatabase(), ioDispatcher = StandardTestDispatcher(testScheduler), ) repository.initialize() advanceUntilIdle() val data = repository.fetchData() assertEquals("Hello world", data) } Repo(...) db.populate()
  87. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository( database = FakeDatabase(), ioDispatcher = StandardTestDispatcher(testScheduler), ) repository.initialize() advanceUntilIdle() val data = repository.fetchData() assertEquals("Hello world", data) } Repo(...) db.populate()
  88. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository( database = FakeDatabase(), ioDispatcher = StandardTestDispatcher(testScheduler), ) repository.initialize() advanceUntilIdle() val data = repository.fetchData() assertEquals("Hello world", data) } Repo(...) db.populate() db.read() suspend fun fetchData() = withContext(ioDispatcher) { database.read() }
  89. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository( database = FakeDatabase(), ioDispatcher = StandardTestDispatcher(testScheduler), ) repository.initialize() advanceUntilIdle() val data = repository.fetchData() assertEquals("Hello world", data) } Repo(...) db.populate() db.read() assert()
  90. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository( database = FakeDatabase(), ioDispatcher = StandardTestDispatcher(testScheduler), ) repository.initialize() advanceUntilIdle() val data = repository.fetchData() assertEquals("Hello world", data) } fun initialize() { scope.launch { database.populate() } } This is a bad API, don’t do this
  91. Injecting dispatchers @Test fun repoTest() = runTest { val repository

    = Repository( database = FakeDatabase(), ioDispatcher = StandardTestDispatcher(testScheduler), ) repository.initialize().await(). val data = repository.fetchData() assertEquals("Hello world", data) } fun initialize(): Deferred<Unit> { return scope.async { } } database.populate()
  92. suspend fun initialize() { } Injecting dispatchers @Test fun repoTest()

    = runTest { val repository = Repository( database = FakeDatabase(), ioDispatcher = StandardTestDispatcher(testScheduler), ) repository.initialize() val data = repository.fetchData() assertEquals("Hello world", data) } database.populate()
  93. Handling the Main dispatcher class HomeViewModel : ViewModel() { private

    val _message = MutableStateFlow("") val message: StateFlow<String> get() = _message fun loadMessage() { viewModelScope.launch { _message.value = "Greetings!" } } }
  94. Handling the Main dispatcher class HomeViewModel : ViewModel() { private

    val _message = MutableStateFlow("") val message: StateFlow<String> get() = _message fun loadMessage() { viewModelScope.launch { _message.value = "Greetings!" } } }
  95. Handling the Main dispatcher } val viewModel = HomeViewModel() viewModel.loadMessage()

    assertEquals("Greetings!", viewModel.message.value) @Test fun testGreeting() = runTest {
  96. Handling the Main dispatcher } val viewModel = HomeViewModel() viewModel.loadMessage()

    assertEquals("Greetings!", viewModel.message.value) @Test fun testGreeting() = runTest { java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
  97. Handling the Main dispatcher } val viewModel = HomeViewModel() viewModel.loadMessage()

    assertEquals("Greetings!", viewModel.message.value) @Test fun testGreeting() = runTest { java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
  98. Handling the Main dispatcher val testDispatcher = UnconfinedTestDispatcher(testScheduler) Dispatchers.setMain(testDispatcher) val

    viewModel = HomeViewModel() viewModel.loadMessage() assertEquals("Greetings!", viewModel.message.value) } @Test fun testGreeting() = runTest { java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
  99. Handling the Main dispatcher val testDispatcher = UnconfinedTestDispatcher(testScheduler) Dispatchers.setMain(testDispatcher) val

    viewModel = HomeViewModel() viewModel.loadMessage() assertEquals("Greetings!", viewModel.message.value) } @Test fun testGreeting() = runTest { java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
  100. java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize.

    For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used Handling the Main dispatcher val testDispatcher = UnconfinedTestDispatcher(testScheduler) Dispatchers.setMain(testDispatcher) try { } finally { Dispatchers.resetMain() } val viewModel = HomeViewModel() viewModel.loadMessage() assertEquals("Greetings!", viewModel.message.value) } @Test fun testGreeting() = runTest {
  101. Handling the Main dispatcher @Test fun testGreeting() = runTest {

    val testDispatcher = UnconfinedTestDispatcher(testScheduler) Dispatchers.setMain(testDispatcher) try { val viewModel = HomeViewModel() viewModel.loadMessage() assertEquals("Greetings!", viewModel.message.value) } finally { Dispatchers.resetMain() } }
  102. Handling the Main dispatcher @Test fun testGreeting() = runTest {

    val testDispatcher = UnconfinedTestDispatcher(testScheduler) Dispatchers.setMain(testDispatcher) try { val viewModel = HomeViewModel() viewModel.loadMessage() assertEquals("Greetings!", viewModel.message.value) } finally { Dispatchers.resetMain() } } fun loadMessage() { viewModelScope.launch { _message.value = "Greetings!" } }
  103. Handling the Main dispatcher @Test fun testGreeting() = runTest {

    val testDispatcher = UnconfinedTestDispatcher(testScheduler) try { val viewModel = HomeViewModel() viewModel.loadMessage() assertEquals("Greetings!", viewModel.message.value) } finally { } } Dispatchers.setMain(testDispatcher) Dispatchers.resetMain()
  104. Handling the Main dispatcher class MainDispatcherRule( val testDispatcher: TestDispatcher =

    UnconfinedTestDispatcher(), ) : TestWatcher() { override fun starting(description: Description) { } override fun finished(description: Description) { } } Dispatchers.setMain(testDispatcher) Dispatchers.resetMain()
  105. Handling the Main dispatcher class HomeViewModelTestUsingRule { @get:Rule val mainDispatcherRule

    = MainDispatcherRule() @Test fun testGreeting() = runTest { val viewModel = HomeViewModel() viewModel.loadMessage() assertEquals("Greetings!", viewModel.message.value) } }
  106. class HomeViewModelTestUsingRule { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun

    testGreeting() = runTest { Handling the Main dispatcher val viewModel = HomeViewModel() viewModel.loadMessage() assertEquals("Greetings!", viewModel.message.value) } }
  107. Handling the Main dispatcher val viewModel = HomeViewModel() viewModel.loadMessage() assertEquals("Greetings!",

    viewModel.message.value) If you replace the Main dispatcher with a TestDispatcher, all new TestDispatchers will automatically share its scheduler class HomeViewModelTestUsingRule { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun testGreeting() = runTest { } }
  108. Handling the Main dispatcher val unconfinedDispatcher = UnconfinedTestDispatcher() val standardDispatcher

    = StandardTestDispatcher() If you replace the Main dispatcher with a TestDispatcher, all new TestDispatchers will automatically share its scheduler class HomeViewModelTestUsingRule { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun testGreeting() = runTest { } }
  109. Flows interface DataSource { fun counts(): Flow<Int> } class Repository(private

    val dataSource: DataSource) { fun scores(): Flow<Int> { return dataSource.counts().map { it * 10 } } }
  110. Flows interface DataSource { fun counts(): Flow<Int> } class Repository(private

    val dataSource: DataSource) { fun scores(): Flow<Int> { return dataSource.counts().map { it * 10 } } }
  111. Flows interface DataSource { fun counts(): Flow<Int> } class Repository(private

    val dataSource: DataSource) { fun scores(): Flow<Int> { return dataSource.counts().map { it * 10 } } } class ColdFakeDataSource : DataSource { override fun counts(): Flow<Int> { return flowOf(1, 2, 3, 4) } }
  112. Flows @Test fun useTerminalOperators() = runTest { val repository =

    Repository(ColdFakeDataSource()) val first = repository.scores().first() assertEquals(10, first) }
  113. Flows @Test fun useTerminalOperators() = runTest { val repository =

    Repository(ColdFakeDataSource()) val first = repository.scores().first() assertEquals(10, first) val values = repository.scores().toList() assertEquals(10, values[0]) assertEquals(20, values[1]) assertEquals(4, values.size) }
  114. Flows @Test fun useTerminalOperators() = runTest { val repository =

    Repository(ColdFakeDataSource()) val first = repository.scores().first() assertEquals(10, first) val values = repository.scores().toList() assertEquals(10, values[0]) assertEquals(20, values[1]) assertEquals(4, values.size) val someValues = repository.scores().take(2).toList() assertEquals(10, someValues[0]) assertEquals(20, someValues[1]) }
  115. Flows class HotFakeDataSource : DataSource { private val flow =

    MutableSharedFlow<Int>() suspend fun emit(value: Int) = flow.emit(value) override fun counts(): Flow<Int> = flow }
  116. Flows class HotFakeDataSource : DataSource { private val flow =

    MutableSharedFlow<Int>() suspend fun emit(value: Int) = flow.emit(value) override fun counts(): Flow<Int> = flow }
  117. Flows class HotFakeDataSource : DataSource { private val flow =

    MutableSharedFlow<Int>() suspend fun emit(value: Int) = flow.emit(value) override fun counts(): Flow<Int> = flow }
  118. Flows @Test fun continuouslyCollect() = runTest { val dataSource =

    HotFakeDataSource() val repository = Repository(dataSource) }
  119. Flows @Test fun continuouslyCollect() = runTest { val dataSource =

    HotFakeDataSource() val repository = Repository(dataSource) val values = mutableListOf<Int>() }
  120. Flows launch(UnconfinedTestDispatcher()) { collect { values += it } }

    @Test fun continuouslyCollect() = runTest { val dataSource = HotFakeDataSource() val repository = Repository(dataSource) val values = mutableListOf<Int>() } repository.scores().
  121. Flows launch(UnconfinedTestDispatcher()) { toList(values) } repository.scores(). @Test fun continuouslyCollect() =

    runTest { val dataSource = HotFakeDataSource() val repository = Repository(dataSource) val values = mutableListOf<Int>() }
  122. Flows launch(UnconfinedTestDispatcher()) { } } @Test fun continuouslyCollect() = runTest

    { val dataSource = HotFakeDataSource() val repository = Repository(dataSource) val values = mutableListOf<Int>() dataSource.emit(1) assertEquals(10, values[0]) repository.scores().toList(values) val collectJob =
  123. Flows val collectJob = launch(UnconfinedTestDispatcher()) { } } collectJob.cancel() @Test

    fun continuouslyCollect() = runTest { val dataSource = HotFakeDataSource() val repository = Repository(dataSource) val values = mutableListOf<Int>() dataSource.emit(1) assertEquals(10, values[0]) repository.scores().toList(values)
  124. Flows val collectJob = launch(UnconfinedTestDispatcher()) { repository.scores().toList(values) } dataSource.emit(1) assertEquals(10,

    values[0]) dataSource.emit(2) dataSource.emit(3) assertEquals(30, values.last()) assertEquals(3, values.size) } collectJob.cancel() @Test fun continuouslyCollect() = runTest { val dataSource = HotFakeDataSource() val repository = Repository(dataSource) val values = mutableListOf<Int>()
  125. interface MyRepository { fun scores(): Flow<Int> } class MyViewModel(private val

    myRepository: MyRepository) : ViewModel() { private val _data = MutableStateFlow(0) val data: StateFlow<Int> = _data.asStateFlow() fun initialize() { viewModelScope.launch { myRepository.scores().collect { score -> _data.value = score } } } } StateFlows
  126. interface MyRepository { fun scores(): Flow<Int> } class MyViewModel(private val

    myRepository: MyRepository) : ViewModel() { private val _data = MutableStateFlow(0) val data: StateFlow<Int> = _data.asStateFlow() fun initialize() { viewModelScope.launch { myRepository.scores().collect { score -> _data.value = score } } } } StateFlows
  127. interface MyRepository { fun scores(): Flow<Int> } class MyViewModel(private val

    myRepository: MyRepository) : ViewModel() { private val _data = MutableStateFlow(0) val data: StateFlow<Int> = _data.asStateFlow() fun initialize() { viewModelScope.launch { myRepository.scores().collect { score -> _data.value = score } } } } StateFlows
  128. interface MyRepository { fun scores(): Flow<Int> } class MyViewModel(private val

    myRepository: MyRepository) : ViewModel() { private val _data = MutableStateFlow(0) val data: StateFlow<Int> = _data.asStateFlow() fun initialize() { viewModelScope.launch { myRepository.scores().collect { score -> _data.value = score } } } } StateFlows
  129. StateFlows class HotFakeRepository : MyRepository { private val flow =

    MutableSharedFlow<Int>() suspend fun emit(value: Int) = flow.emit(value) override fun scores(): Flow<Int> = flow }
  130. @get:Rule val mainDispatcherRule = MainDispatcherRule() StateFlows @Test fun testHotFakeRepository() =

    runTest { val fakeRepository = HotFakeRepository() val viewModel = MyViewModel(fakeRepository) }
  131. StateFlows @Test fun testHotFakeRepository() = runTest { val fakeRepository =

    HotFakeRepository() val viewModel = MyViewModel(fakeRepository) }
  132. StateFlows @Test fun testHotFakeRepository() = runTest { val fakeRepository =

    HotFakeRepository() val viewModel = MyViewModel(fakeRepository) assertEquals(0, viewModel.data.value) }
  133. StateFlows @Test fun testHotFakeRepository() = runTest { val fakeRepository =

    HotFakeRepository() val viewModel = MyViewModel(fakeRepository) assertEquals(0, viewModel.data.value) viewModel.initialize() } fun initialize() { viewModelScope.launch { myRepository.scores().collect { score -> _data.value = score } } }
  134. StateFlows @Test fun testHotFakeRepository() = runTest { val fakeRepository =

    HotFakeRepository() val viewModel = MyViewModel(fakeRepository) assertEquals(0, viewModel.data.value) viewModel.initialize() fakeRepository.emit(1) assertEquals(1, viewModel.data.value) }
  135. StateFlows @Test fun testHotFakeRepository() = runTest { val fakeRepository =

    HotFakeRepository() val viewModel = MyViewModel(fakeRepository) assertEquals(0, viewModel.data.value) viewModel.initialize() fakeRepository.emit(1) assertEquals(1, viewModel.data.value) fakeRepository.emit(2) fakeRepository.emit(3) assertEquals(3, viewModel.data.value) }
  136. StateFlows @Test fun testHotFakeRepository() = runTest { val fakeRepository =

    HotFakeRepository() val viewModel = MyViewModel(fakeRepository) assertEquals(0, viewModel.data.value) viewModel.initialize() fakeRepository.emit(1) assertEquals(1, viewModel.data.value) fakeRepository.emit(2) fakeRepository.emit(3) assertEquals(3, viewModel.data.value) }
  137. StateFlows @Test fun testHotFakeRepository() = runTest { val fakeRepository =

    HotFakeRepository() val viewModel = MyViewModel(fakeRepository) assertEquals(0, viewModel.data.value) viewModel.initialize() fakeRepository.emit(1) assertEquals(1, viewModel.data.value) fakeRepository.emit(2) fakeRepository.emit(3) assertEquals(3, viewModel.data.value) }
  138. StateFlows with stateIn private val _data = MutableStateFlow(0) _data.asStateFlow() fun

    initialize() { .launch { .collect { count -> _data.value = count } } } myRepository: MyRepository myRepository.scores() class MyViewModel( ) : ViewModel() { private val viewModelScope val data: StateFlow<Int> = }
  139. StateFlows with stateIn @Test fun testLazilySharingViewModel() = runTest { val

    fakeRepository = HotFakeRepository() val viewModel = MyViewModel(fakeRepository) assertEquals(0, viewModel.data.value) fakeRepository.emit(1) assertEquals(1, viewModel.data.value) }
  140. StateFlows with stateIn @Test fun testLazilySharingViewModel() = runTest { val

    fakeRepository = HotFakeRepository() val viewModel = MyViewModel(fakeRepository) assertEquals(0, viewModel.data.value) fakeRepository.emit(1) assertEquals(1, viewModel.data.value) }
  141. StateFlows with stateIn class MyViewModel( myRepository: MyRepository ) : ViewModel()

    { val data: StateFlow<Int> = myRepository.scores() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0) }
  142. StateFlows with stateIn class MyViewModel( myRepository: MyRepository ) : ViewModel()

    { val data: StateFlow<Int> = myRepository.scores() .stateIn(viewModelScope, , 0) } Test ViewModel Fake Repository SharingStarted.WhileSubscribed(5000)
  143. StateFlows with stateIn class MyViewModel( myRepository: MyRepository ) : ViewModel()

    { val data: StateFlow<Int> = myRepository.scores() .stateIn(viewModelScope, , 0) } Test ViewModel Fake Repository SharingStarted.WhileSubscribed(5000)
  144. StateFlows with stateIn class MyViewModel( myRepository: MyRepository ) : ViewModel()

    { val data: StateFlow<Int> = myRepository.scores() .stateIn(viewModelScope, , 0) } Test ViewModel Fake Repository SharingStarted.WhileSubscribed(5000)
  145. StateFlows with stateIn class MyViewModel( myRepository: MyRepository ) : ViewModel()

    { val data: StateFlow<Int> = myRepository.scores() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0) } Test ViewModel Fake Repository
  146. StateFlows with stateIn class MyViewModel( myRepository: MyRepository ) : ViewModel()

    { val data: StateFlow<Int> = myRepository.scores() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0) } Test ViewModel Fake Repository
  147. StateFlows with stateIn class MyViewModel( myRepository: MyRepository ) : ViewModel()

    { val data: StateFlow<Int> = myRepository.scores() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0) } Test ViewModel Fake Repository
  148. StateFlows with stateIn class MyViewModel( myRepository: MyRepository ) : ViewModel()

    { val data: StateFlow<Int> = myRepository.scores() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0) } Test ViewModel Fake Repository
  149. StateFlows with stateIn class MyViewModel( myRepository: MyRepository ) : ViewModel()

    { val data: StateFlow<Int> = myRepository.scores() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0) } SharingStarted.Lazily Test ViewModel Fake Repository
  150. StateFlows with stateIn @Test fun testLazilySharingViewModel() = runTest { val

    fakeRepository = HotFakeRepository() val viewModel = MyViewModel(fakeRepository) assertEquals(0, viewModel.data.value) fakeRepository.emit(1) assertEquals(1, viewModel.data.value) }
  151. StateFlows with stateIn val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.data.collect() }

    assertEquals(0, viewModel.data.value) fakeRepository.emit(1) assertEquals(1, viewModel.data.value) } collectJob.cancel() @Test fun testLazilySharingViewModel() = runTest { val fakeRepository = HotFakeRepository() val viewModel = MyViewModel(fakeRepository)
  152. Summary • Use runTest for tests with coroutines • Inject

    dispatchers into your classes to make them testable • Create TestDispatchers as needed, always share a single scheduler • Replace the Main dispatcher in unit tests (also simplifies sharing!)