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

Untangling Coroutine Testing (KotlinConf '23)

Untangling Coroutine Testing (KotlinConf '23)

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.

More info and resources: https://zsmb.co/appearances/kotlinconf-2023-day1/

Márton Braun

April 13, 2023
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. Testing suspending functions suspend fun fetchData(): String { delay(1000L) return

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

    "Hello world" } @Test fun dataIsHelloWorld() { val data = fetchData() assertEquals("Hello world", data) }
  4. 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
  5. 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
  6. 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
  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 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
  10. 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 *
  11. 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
  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) } Test thread Dispatchers.IO fetchData()
  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) } Test thread Dispatchers.IO fetchData()
  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 fetchData() delay()
  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() delay()
  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() delay() assert()
  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() assert()
  18. Testing new coroutines @Test fun directExample() = runTest { val

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

    repo = UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals( listOf("Alice", "Bob"), repo.getAllUsers() ) }
  20. 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() ) }
  21. 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() ) }
  22. 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() ) }
  23. 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() ) }
  24. • Queues up coroutines on the scheduler • You need

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

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

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

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

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

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } UserRepo()
  30. 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")
  31. 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")
  32. 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")
  33. 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")
  34. StandardTestDispatcher now now 80 ms 90 ms 100 ms 500

    ms 750 ms • You need to advance those coroutines manually
  35. StandardTestDispatcher • runCurrent() now now 80 ms 90 ms 100

    ms 500 ms 750 ms • You need to advance those coroutines manually
  36. StandardTestDispatcher • runCurrent() now now 80 ms 90 ms 100

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

    90 ms 100 ms 500 ms 750 ms • You need to advance those coroutines manually
  38. StandardTestDispatcher now now 80 ms 90 ms 100 ms 500

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

    ms 90 ms 100 ms 500 ms 750 ms • You need to advance those coroutines manually
  40. • You need to advance those coroutines manually StandardTestDispatcher •

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

    runCurrent() • advanceTimeBy(delayTimeMillis: Long) • advanceUntilIdle() now now 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. 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 @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 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. 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 • 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 • As the Main dispatcher • For coroutines that collect Flows 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. Dispatchers.IO Injecting dispatchers class Repository( private val ioDispatcher: CoroutineContext =

    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 ,
  80. 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 ,
  81. FakeDatabase(), @Test fun repoTest() = runTest { val repository =

    Repository( ) repository.initialize() val data = repository.fetchData() assertEquals("Hello world", data) } Injecting dispatchers
  82. 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()
  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) } this: TestScope
  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) }
  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(...)
  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) } fun initialize() { scope.launch { database.populate() } } Repo(...)
  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() fun initialize() { scope.launch { database.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()
  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()
  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) } Repo(...) db.populate() db.read() suspend fun fetchData() = withContext(ioDispatcher) { database.read() }
  91. 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()
  92. 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
  93. 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()
  94. 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()
  95. Handling the Main dispatcher class HomeViewModel : ViewModel() { private

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

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

    assertEquals("Greetings!", viewModel.message.value) @Test fun testGreeting() = runTest {
  98. 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
  99. 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
  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 { java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
  101. 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 this: TestScope
  102. 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 {
  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() } }
  104. 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!" } }
  105. 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()
  106. 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()
  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) } }
  108. 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) } }
  109. 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 { } }
  110. Handling the Main dispatcher val unconfinedDispatcher = UnconfinedTestDispatcher() val standardDispatcher

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

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

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

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

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

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

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

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

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

    runTest { val dataSource = HotFakeDataSource() val repository = Repository(dataSource) val values = mutableListOf<Int>() }
  124. 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 =
  125. 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 =
  126. Flows val collectJob = 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) launch(UnconfinedTestDispatcher()) { } }
  127. Flows @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) this: TestScope backgroundScope.launch(UnconfinedTestDispatcher()) { } }
  128. backgroundScope.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) } @Test fun continuouslyCollect() = runTest { val dataSource = HotFakeDataSource() val repository = Repository(dataSource) val values = mutableListOf<Int>() Flows
  129. 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
  130. 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
  131. 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
  132. 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
  133. StateFlows class HotFakeRepository : MyRepository { private val flow =

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

    runTest { val fakeRepository = HotFakeRepository() val viewModel = MyViewModel(fakeRepository) }
  135. @get:Rule val mainDispatcherRule = MainDispatcherRule() StateFlows @Test fun testHotFakeRepository() =

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

    HotFakeRepository() val viewModel = MyViewModel(fakeRepository) assertEquals(0, viewModel.data.value) }
  137. 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 } } }
  138. 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) }
  139. 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) }
  140. 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) }
  141. 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) }
  142. 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> = }
  143. 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) }
  144. 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) }
  145. StateFlows with stateIn class MyViewModel( myRepository: MyRepository ) : ViewModel()

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

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

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

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

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

    { val data: StateFlow<Int> = myRepository.scores() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0) } Test ViewModel Fake Repository
  151. 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
  152. 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)
  153. assertEquals(0, viewModel.data.value) fakeRepository.emit(1) assertEquals(1, viewModel.data.value) StateFlows with stateIn @Test fun

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

    testLazilySharingViewModel() = runTest { val fakeRepository = HotFakeRepository() val viewModel = MyViewModel(fakeRepository) backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.data.collect() } }
  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!) • Use backgroundScope to create Flow collectors
  156. Thank you, and don’t forget to vote! KotlinConf’23 Amsterdam Márton

    Braun @zsmb13 Developer Relations Engineer Google