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.

4047c64e3a1e2f81addd4ba675ddc451?s=128

Marton Braun

April 25, 2022
Tweet

More Decks by Marton Braun

Other Decks in Programming

Transcript

  1. Márton Braun @zsmb13 Developer Relations Engineer Untangling Coroutine Testing

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

    • Best practices • Handling the Main dispatcher • Flows & StateFlows
  3. Introduction What’s up with testing coroutines anyway?

  4. Introduction

  5. Introduction

  6. kotlinx.coroutines.test Pre-1.6 APIs 1.6+ APIs

  7. kotlinx.coroutines.test Pre-1.6 APIs runBlockingTest TestCoroutineDispatcher TestCoroutineScope pauseDispatcher resumeDispatcher 1.6+ APIs

  8. kotlinx.coroutines.test 1.6+ APIs runTest TestDispatcher TestScope TestCoroutineScheduler Pre-1.6 APIs runBlockingTest

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

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

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

    TestCoroutineDispatcher TestCoroutineScope pauseDispatcher resumeDispatcher @Experimental @Deprecated @Experimental
  12. kotlinx.coroutines.test goo.gle/coroutine-test-migration

  13. Testing suspending functions runTest. run.

  14. Testing suspending functions suspend fun fetchData(): String { delay(1000L) return

    "Hello world" }
  15. Testing suspending functions suspend fun fetchData(): String { delay(1000L) return

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

    "Hello world" } @Test fun dataIsHelloWorld() { val data = fetchData() assertEquals("Hello world", data) }
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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 *
  24. 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
  25. 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()
  26. 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()
  27. 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()
  28. 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()
  29. 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()
  30. 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()
  31. Testing new coroutines

  32. Testing new coroutines @Test fun directExample() = runTest { val

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

    repo = UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals( listOf("Alice", "Bob"), repo.getAllUsers() ) }
  34. 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() ) }
  35. 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() ) }
  36. 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() ) }
  37. 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() ) }
  38. The coroutine test APIs The latest and greatest in kotlinx.coroutines.test

  39. Test APIs runTest

  40. Test APIs runTest TestScope

  41. Test APIs runTest TestDispatcher TestScope

  42. Test APIs runTest TestDispatcher TestCoroutineScheduler TestScope

  43. Test APIs runTest TestDispatcher TestCoroutineScheduler TestScope TestDispatcher TestDispatcher

  44. Test APIs runTest TestDispatcher TestCoroutineScheduler TestScope TestDispatcher TestDispatcher All TestDispatchers

    must share the same scheduler
  45. Test APIs TestDispatcher TestCoroutineScheduler

  46. Test APIs TestDispatcher TestCoroutineScheduler StandardTestDispatcher UnconfinedTestDispatcher

  47. StandardTestDispatcher

  48. • Queues up coroutines on the scheduler StandardTestDispatcher

  49. • Queues up coroutines on the scheduler • You need

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

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

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

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

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

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } UserRepo()
  55. 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")
  56. 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")
  57. 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")
  58. 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")
  59. • You need to advance those coroutines manually StandardTestDispatcher

  60. • You need to advance those coroutines manually StandardTestDispatcher 80

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

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

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

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

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

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

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

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

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

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

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

    UserRepository() launch { repo.register("Alice") } launch { repo.register("Bob") } advanceUntilIdle() assertEquals(listOf("Alice", "Bob"), repo.getAllUsers()) } UserRepo()
  72. 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")
  73. 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")
  74. 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")
  75. 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")
  76. 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")
  77. 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()
  78. 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()
  79. UnconfinedTestDispatcher

  80. UnconfinedTestDispatcher • Starts new coroutines eagerly (like runBlockingTest)

  81. UnconfinedTestDispatcher • Starts new coroutines eagerly (like runBlockingTest) • Can

    be a good choice for simple tests
  82. UnconfinedTestDispatcher • Starts new coroutines eagerly (like runBlockingTest) • Can

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

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

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

    UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) } UserRepo()
  87. 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")
  88. 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")
  89. 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()
  90. 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()
  91. TestDispatchers

  92. TestDispatchers StandardTestDispatcher Queues up new coroutines Use by default

  93. TestDispatchers StandardTestDispatcher Queues up new coroutines Use by default UnconfinedTestDispatcher

    Starts new coroutines eagerly Use selectively
  94. 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
  95. Injecting dispatchers Something something heat exchangers

  96. 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() } }
  97. 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() } }
  98. 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() } }
  99. Injecting dispatchers @Test fun repoTest() = runTest { val repository

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

    = Repository(FakeDatabase()) repository.initialize() val data = repository.fetchData() assertEquals("Hello world", data) } Test thread Dispatchers.IO
  101. 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
  102. 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() } }
  103. 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() }
  104. 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()
  105. 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) }
  106. 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) }
  107. 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) }
  108. , 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
  109. 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 ,
  110. FakeDatabase(), @Test fun repoTest() = runTest { val repository =

    Repository( ) repository.initialize() val data = repository.fetchData() assertEquals("Hello world", data) } Injecting dispatchers
  111. 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()
  112. 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
  113. 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) }
  114. 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(...)
  115. 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() } }
  116. 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(...)
  117. 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()
  118. 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() }
  119. 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()
  120. 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
  121. 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()
  122. 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()
  123. Keeping track of coroutines Wouldn’t want to lose a Job

  124. Keeping track of coroutines • runTest will wait for coroutines

    to complete if they are either:
  125. Keeping track of coroutines • runTest will wait for coroutines

    to complete if they are either: • Children of the test coroutine (up to a timeout)
  126. 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
  127. Keeping track of coroutines @Test fun uncompletedJobs() = runTest {

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

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

    private val scope = CoroutineScope(ioDispatcher) fun shutdown() { scope.launch { // Do some async cleanup } } }
  130. 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 } }
  131. 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 } }
  132. Handling the Main dispatcher A UI thread without a UI

  133. Handling the Main dispatcher class HomeViewModel : ViewModel() { private

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

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

    assertEquals("Greetings!", viewModel.message.value) @Test fun testGreeting() = runTest {
  136. 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 {
  137. 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 {
  138. 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() } }
  139. 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!" } }
  140. 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()
  141. 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()
  142. 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) } }
  143. 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
  144. Managing test objects Creating dispatchers and schedulers and scopes

  145. Using TestDispatchers in the test class Repository(private val ioDispatcher: CoroutineDispatcher)

    { /* ... */ }
  146. Using TestDispatchers in the test class Repository(private val ioDispatcher: CoroutineDispatcher)

    { /* ... */ } class DispatcherTypesTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() }
  147. 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 } }
  148. 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) } }
  149. 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 }
  150. 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()) } }
  151. Using TestDispatchers outside the test

  152. Using TestDispatchers outside the test class RepositoryTest { @get:Rule val

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

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

    mainDispatcherRule = MainDispatcherRule() private val repository = Repository(UnconfinedTestDispatcher()) } @Test fun someRepositoryTest() = runTest { // Test the repository... }
  155. 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
  156. 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
  157. Creating test objects class ExampleTest { runTest { // ...

    } } @Test fun someTest() =
  158. Creating test objects class ExampleTest { ) testScope.runTest { //

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

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

    testScheduler) testDispatcher) testScope.runTest { // ... } } @Test fun someTest() = val testScope = TestScope( val testDispatcher = StandardTestDispatcher(
  161. Flows Suspenseful streams

  162. Flows interface DataSource { fun counts(): Flow<Int> } class Repository(private

    val dataSource: DataSource) { fun scores(): Flow<Int> { return dataSource.counts().map { it * 10 } } }
  163. Flows

  164. Flows interface DataSource { fun counts(): Flow<Int> } class Repository(private

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

    Repository(ColdFakeDataSource()) }
  167. Flows @Test fun useTerminalOperators() = runTest { val repository =

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

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

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

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

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

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

    HotFakeDataSource() val repository = Repository(dataSource) val values = mutableListOf<Int>() }
  176. 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 } } }
  177. 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 } } }
  178. 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 } } }
  179. Flows @Test fun continuouslyCollect() = runTest { val dataSource =

    HotFakeDataSource() val repository = Repository(dataSource) val values = mutableListOf<Int>() val collectJob = launch(UnconfinedTestDispatcher()) { repository.scores().toList(values) } }
  180. 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]) }
  181. 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()
  182. 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()
  183. Flows with Turbine

  184. Flows with Turbine @Test fun usingTurbine() = runTest { val

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

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

    dataSource = HotFakeDataSource() val repository = Repository(dataSource) repository.scores().test { dataSource.emit(1) assertEquals(10, awaitItem()) } }
  187. 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()) } }
  188. StateFlows Look! It’s a stream! It’s a state holder!

  189. 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
  190. 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
  191. 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
  192. 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
  193. StateFlows

  194. StateFlows class ColdFakeRepository : MyRepository { override fun scores() =

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

    runTest { val viewModel = MyViewModel(ColdFakeRepository()) }
  196. 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 } } }
  197. StateFlows @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun testStateFlow() =

    runTest { val viewModel = MyViewModel(ColdFakeRepository()) viewModel.initialize() assertEquals(1, viewModel.data.value) }
  198. 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) }
  199. 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) }
  200. 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) }
  201. Summary What did we learn today?

  202. 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!)
  203. Resources • goo.gle/coroutine-test-guide • goo.gle/coroutine-test-migration • github.com/cashapp/turbine NEW!

  204. Thank You! Márton Braun @zsmb13 Untangling Coroutine Testing