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

Untangling Coroutine Testing (Android Makers 2022)

Untangling Coroutine Testing (Android Makers 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.

Márton Braun

April 25, 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("Alice") reg("Bob")
  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("Alice") reg("Bob")
  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("Alice") reg("Bob")
  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 •

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

    runCurrent() • advanceTimeBy(delayTimeMillis: Long) 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("Alice") reg("Bob")
  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. 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")
  51. 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()
  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. UnconfinedTestDispatcher • Starts new coroutines eagerly (like runBlockingTest) • Can

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

    UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) }
  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()) } this: TestScope
  57. UnconfinedTestDispatcher @Test fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) { val userRepo =

    UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) } UserRepo()
  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() reg("Alice")
  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") reg("Bob")
  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") assert()
  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. TestDispatchers StandardTestDispatcher Queues up new coroutines Use by default UnconfinedTestDispatcher

    Starts new coroutines eagerly Use selectively • While migrating tests from old APIs • As the Main dispatcher • For coroutines that collect values
  63. 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() } }
  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 @Test fun repoTest() = runTest { val repository

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

    = Repository(FakeDatabase()) repository.initialize() val data = repository.fetchData() assertEquals("Hello world", data) } Test thread Dispatchers.IO
  68. 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
  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 db.populate() fun initialize() { scope.launch { database.populate() } }
  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.read() db.populate() suspend fun fetchData() = withContext(Dispatchers.IO) { database.read() }
  71. 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()
  72. 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) }
  73. 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) }
  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. , 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
  76. 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 ,
  77. FakeDatabase(), @Test fun repoTest() = runTest { val repository =

    Repository( ) repository.initialize() val data = repository.fetchData() assertEquals("Hello world", data) } Injecting dispatchers
  78. 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()
  79. 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
  80. 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) }
  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) } Repo(...)
  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) } Repo(...) db.populate() fun initialize() { scope.launch { database.populate() } }
  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) } db.populate() 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) } Repo(...) db.populate()
  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() db.read() suspend fun fetchData() = withContext(ioDispatcher) { database.read() }
  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() db.read() assert()
  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) } fun initialize() { scope.launch { database.populate() } } This is a bad API, don’t do this
  88. 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()
  89. 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()
  90. Keeping track of coroutines • runTest will wait for coroutines

    to complete if they are either: • Children of the test coroutine (up to a timeout)
  91. Keeping track of coroutines • runTest will wait for coroutines

    to complete if they are either: • Children of the test coroutine (up to a timeout) • Running on the test scheduler
  92. Keeping track of coroutines @Test fun uncompletedJobs() = runTest {

    launch(Dispatchers.IO) { delay(500L) // Do some work... } }
  93. Keeping track of coroutines @Test fun uncompletedJobs() = runTest(dispatchTimeoutMs =

    1000L) { launch(Dispatchers.IO) { delay(500L) // Do some work... } }
  94. Keeping track of coroutines class SocketService(private val ioDispatcher: CoroutineDispatcher) {

    private val scope = CoroutineScope(ioDispatcher) fun shutdown() { scope.launch { // Do some async cleanup } } }
  95. Keeping track of coroutines @Test fun cleanupTest() = runTest {

    val testDispatcher = StandardTestDispatcher(testScheduler) val service = SocketService(testDispatcher) // Do some testing... service.shutdown() } fun shutdown() { scope.launch { // Do some async cleanup } }
  96. Keeping track of coroutines @Test fun cleanupTest() = runTest {

    val testDispatcher = StandardTestDispatcher(testScheduler) val service = SocketService(testDispatcher) // Do some testing... service.shutdown() } this: TestScope fun shutdown() { scope.launch { // Do some async cleanup } }
  97. Handling the Main dispatcher class HomeViewModel : ViewModel() { private

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

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

    assertEquals("Greetings!", viewModel.message.value) @Test fun testGreeting() = runTest {
  100. 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 {
  101. 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 {
  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() } }
  103. 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!" } }
  104. 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()
  105. 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()
  106. 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) } }
  107. 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) } } If you replace the Main dispatcher with a TestDispatcher, all new TestDispatchers will automatically share its scheduler
  108. Using TestDispatchers in the test class Repository(private val ioDispatcher: CoroutineDispatcher)

    { /* ... */ } class DispatcherTypesTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() }
  109. Using TestDispatchers in the test class Repository(private val ioDispatcher: CoroutineDispatcher)

    { /* ... */ } class DispatcherTypesTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun injectingTestDispatchers() = runTest { // Shares Main’s scheduler } }
  110. Using TestDispatchers in the test class Repository(private val ioDispatcher: CoroutineDispatcher)

    { /* ... */ } class DispatcherTypesTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun injectingTestDispatchers() = runTest { val unconfinedRepo = Repository(mainDispatcherRule.testDispatcher) } }
  111. Using TestDispatchers in the test class Repository(private val ioDispatcher: CoroutineDispatcher)

    { /* ... */ } class DispatcherTypesTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun injectingTestDispatchers() = runTest { val unconfinedRepo = Repository(mainDispatcherRule.testDispatcher) val standardRepo = Repository(StandardTestDispatcher()) } // ^ Shares Main’s scheduler }
  112. Using TestDispatchers in the test class Repository(private val ioDispatcher: CoroutineDispatcher)

    { /* ... */ } class DispatcherTypesTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun injectingTestDispatchers() = runTest { val unconfinedRepo = Repository(UnconfinedTestDispatcher()) val standardRepo = Repository(StandardTestDispatcher()) } }
  113. Using TestDispatchers outside the test class RepositoryTest { @get:Rule val

    mainDispatcherRule = MainDispatcherRule() private val repository = Repository(???) @Test fun someRepositoryTest() = runTest { // Test the repository... } }
  114. Using TestDispatchers outside the test class RepositoryTest { @get:Rule val

    mainDispatcherRule = MainDispatcherRule() private val repository = Repository(UnconfinedTestDispatcher()) @Test fun someRepositoryTest() = runTest { // Test the repository... } }
  115. Using TestDispatchers outside the test class RepositoryTest { @get:Rule val

    mainDispatcherRule = MainDispatcherRule() private val repository = Repository(UnconfinedTestDispatcher()) } @Test fun someRepositoryTest() = runTest { // Test the repository... }
  116. Using TestDispatchers outside the test class RepositoryTest { @get:Rule val

    mainDispatcherRule = MainDispatcherRule() private val repository = Repository( ) } @Test fun someRepositoryTest() = runTest { // Test the repository... } mainDispatcherRule.testDispatcher
  117. Using TestDispatchers outside the test class RepositoryTest { @get:Rule val

    mainDispatcherRule = MainDispatcherRule() private val repository = Repository( StandardTestDispatcher( .scheduler) ) } @Test fun someRepositoryTest() = runTest { // Test the repository... } mainDispatcherRule.testDispatcher
  118. Creating test objects class ExampleTest { ) testScope.runTest { //

    ... } } @Test fun someTest() = val testScope = TestScope(
  119. Creating test objects class ExampleTest { ) testDispatcher) testScope.runTest {

    // ... } } @Test fun someTest() = val testScope = TestScope( val testDispatcher = StandardTestDispatcher(
  120. Creating test objects class ExampleTest { val testScheduler = TestCoroutineScheduler()

    testScheduler) testDispatcher) testScope.runTest { // ... } } @Test fun someTest() = val testScope = TestScope( val testDispatcher = StandardTestDispatcher(
  121. Flows interface DataSource { fun counts(): Flow<Int> } class Repository(private

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

    val dataSource: DataSource) { fun scores(): Flow<Int> { return dataSource.counts().map { it * 10 } } }
  123. 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) } }
  124. Flows @Test fun useTerminalOperators() = runTest { val repository =

    Repository(ColdFakeDataSource()) val first = repository.scores().first() assertEquals(10, first) }
  125. 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) }
  126. 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]) }
  127. Flows class HotFakeDataSource : DataSource { private val flow =

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

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

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

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

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

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

    HotFakeDataSource() val repository = Repository(dataSource) val values = mutableListOf<Int>() val collectJob = launch(UnconfinedTestDispatcher()) { repository.scores().collect { values += it } } }
  134. Flows @Test fun continuouslyCollect() = runTest { val dataSource =

    HotFakeDataSource() val repository = Repository(dataSource) val values = mutableListOf<Int>() val collectJob = launch(UnconfinedTestDispatcher()) { repository.scores().collect { values += it } } }
  135. Flows @Test fun continuouslyCollect() = runTest { val dataSource =

    HotFakeDataSource() val repository = Repository(dataSource) val values = mutableListOf<Int>() val collectJob = launch(UnconfinedTestDispatcher()) { repository.scores().collect { values += it } } }
  136. Flows @Test fun continuouslyCollect() = runTest { val dataSource =

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

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

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

    HotFakeDataSource() val repository = Repository(dataSource) val values = mutableListOf<Int>() 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()
  140. Flows with Turbine @Test fun usingTurbine() = runTest { val

    dataSource = HotFakeDataSource() val repository = Repository(dataSource) }
  141. Flows with Turbine @Test fun usingTurbine() = runTest { val

    dataSource = HotFakeDataSource() val repository = Repository(dataSource) repository.scores().test { } }
  142. Flows with Turbine @Test fun usingTurbine() = runTest { val

    dataSource = HotFakeDataSource() val repository = Repository(dataSource) repository.scores().test { dataSource.emit(1) assertEquals(10, awaitItem()) } }
  143. Flows with Turbine @Test fun usingTurbine() = runTest { val

    dataSource = HotFakeDataSource() val repository = Repository(dataSource) repository.scores().test { dataSource.emit(1) assertEquals(10, awaitItem()) dataSource.emit(2) awaitItem() dataSource.emit(3) assertEquals(30, awaitItem()) } }
  144. 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
  145. 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
  146. 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
  147. 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
  148. StateFlows class ColdFakeRepository : MyRepository { override fun scores() =

    flow { emit(1) delay(100) emit(2) delay(100) emit(3) } }
  149. StateFlows @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun testStateFlow() =

    runTest { val viewModel = MyViewModel(ColdFakeRepository()) }
  150. StateFlows @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun testStateFlow() =

    runTest { val viewModel = MyViewModel(ColdFakeRepository()) viewModel.initialize() } fun initialize() { viewModelScope.launch { myRepository.scores().collect { score -> _data.value = score } } }
  151. StateFlows @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun testStateFlow() =

    runTest { val viewModel = MyViewModel(ColdFakeRepository()) viewModel.initialize() assertEquals(1, viewModel.data.value) }
  152. StateFlows @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun testStateFlow() =

    runTest { val viewModel = MyViewModel(ColdFakeRepository()) viewModel.initialize() assertEquals(1, viewModel.data.value) advanceTimeBy(101) assertEquals(2, viewModel.data.value) }
  153. StateFlows @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun testStateFlow() =

    runTest { val viewModel = MyViewModel(ColdFakeRepository()) viewModel.initialize() assertEquals(1, viewModel.data.value) advanceTimeBy(101) assertEquals(2, viewModel.data.value) advanceTimeBy(101) assertEquals(3, viewModel.data.value) }
  154. StateFlows @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun testStateFlow() =

    runTest { val viewModel = MyViewModel(ColdFakeRepository()) viewModel.initialize() assertEquals(1, viewModel.data.value) advanceTimeBy(101) assertEquals(2, viewModel.data.value) advanceTimeBy(101) assertEquals(3, viewModel.data.value) }
  155. 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!)