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.

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

    View full-size slide

  2. Agenda
    ● Testing suspending functions
    ● The coroutine test APIs
    ● Best practices
    ● Handling the Main dispatcher
    ● Flows & StateFlows

    View full-size slide

  3. Introduction
    What’s up with testing coroutines anyway?

    View full-size slide

  4. Introduction

    View full-size slide

  5. Introduction

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  12. kotlinx.coroutines.test
    goo.gle/coroutine-test-migration

    View full-size slide

  13. Testing suspending functions
    runTest. run.

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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
    *

    View full-size slide

  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

    View full-size slide

  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()

    View full-size slide

  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()

    View full-size slide

  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()

    View full-size slide

  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()

    View full-size slide

  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()

    View full-size slide

  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()

    View full-size slide

  31. Testing new coroutines

    View full-size slide

  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()
    )
    }

    View full-size slide

  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()
    )
    }

    View full-size slide

  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()
    )
    }

    View full-size slide

  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()
    )
    }

    View full-size slide

  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()
    )
    }

    View full-size slide

  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()
    )
    }

    View full-size slide

  38. The coroutine test APIs
    The latest and greatest in kotlinx.coroutines.test

    View full-size slide

  39. Test APIs
    runTest

    View full-size slide

  40. Test APIs
    runTest TestScope

    View full-size slide

  41. Test APIs
    runTest
    TestDispatcher
    TestScope

    View full-size slide

  42. Test APIs
    runTest
    TestDispatcher TestCoroutineScheduler
    TestScope

    View full-size slide

  43. Test APIs
    runTest
    TestDispatcher TestCoroutineScheduler
    TestScope
    TestDispatcher
    TestDispatcher

    View full-size slide

  44. Test APIs
    runTest
    TestDispatcher TestCoroutineScheduler
    TestScope
    TestDispatcher
    TestDispatcher
    All TestDispatchers must share the same scheduler

    View full-size slide

  45. Test APIs
    TestDispatcher
    TestCoroutineScheduler

    View full-size slide

  46. Test APIs
    TestDispatcher
    TestCoroutineScheduler
    StandardTestDispatcher UnconfinedTestDispatcher

    View full-size slide

  47. StandardTestDispatcher

    View full-size slide


  48. Queues up coroutines on the scheduler
    StandardTestDispatcher

    View full-size slide


  49. Queues up coroutines on the scheduler

    You need to advance those coroutines manually
    StandardTestDispatcher

    View full-size slide


  50. Queues up coroutines on the scheduler

    You need to advance those coroutines manually

    runTest uses a StandardTestDispatcher by default
    StandardTestDispatcher
    runTest
    StandardTestDispatcher
    TestScope

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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")

    View full-size slide

  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")

    View full-size slide

  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")

    View full-size slide

  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")

    View full-size slide


  59. You need to advance those coroutines manually
    StandardTestDispatcher

    View full-size slide


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

    View full-size slide


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

    runCurrent()

    View full-size slide


  62. You need to advance those coroutines manually
    StandardTestDispatcher

    runCurrent()
    80
    ms
    90
    ms
    100
    ms
    500
    ms
    750
    ms

    View full-size slide


  63. You need to advance those coroutines manually
    StandardTestDispatcher

    runCurrent()

    advanceTimeBy(delayTimeMillis: Long)
    80
    ms
    90
    ms
    100
    ms
    500
    ms
    750
    ms

    View full-size slide


  64. You need to advance those coroutines manually
    StandardTestDispatcher

    runCurrent()

    advanceTimeBy(delayTimeMillis: Long)
    80
    ms
    90
    ms
    100
    ms
    500
    ms
    750
    ms

    View full-size slide


  65. You need to advance those coroutines manually
    StandardTestDispatcher

    runCurrent()

    advanceTimeBy(delayTimeMillis: Long)
    80
    ms
    90
    ms
    100
    ms
    500
    ms
    750
    ms

    View full-size slide


  66. You need to advance those coroutines manually
    StandardTestDispatcher

    runCurrent()

    advanceTimeBy(delayTimeMillis: Long)

    advanceUntilIdle()
    80
    ms
    90
    ms
    100
    ms
    500
    ms
    750
    ms

    View full-size slide


  67. You need to advance those coroutines manually
    StandardTestDispatcher

    runCurrent()

    advanceTimeBy(delayTimeMillis: Long)

    advanceUntilIdle()
    80
    ms
    90
    ms
    100
    ms
    500
    ms
    750
    ms

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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()

    View full-size slide

  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")

    View full-size slide

  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")

    View full-size slide

  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")

    View full-size slide

  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")

    View full-size slide

  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")

    View full-size slide

  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()

    View full-size slide

  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()

    View full-size slide

  79. UnconfinedTestDispatcher

    View full-size slide

  80. UnconfinedTestDispatcher

    Starts new coroutines eagerly (like runBlockingTest)

    View full-size slide

  81. UnconfinedTestDispatcher

    Starts new coroutines eagerly (like runBlockingTest)

    Can be a good choice for simple tests

    View full-size slide

  82. UnconfinedTestDispatcher

    Starts new coroutines eagerly (like runBlockingTest)

    Can be a good choice for simple tests

    Does not emulate real concurrency

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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()

    View full-size slide

  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")

    View full-size slide

  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")

    View full-size slide

  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()

    View full-size slide

  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()

    View full-size slide

  91. TestDispatchers

    View full-size slide

  92. TestDispatchers
    StandardTestDispatcher
    Queues up new coroutines
    Use by default

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  95. Injecting dispatchers
    Something something heat exchangers

    View full-size slide

  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()
    }
    }

    View full-size slide

  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()
    }
    }

    View full-size slide

  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()
    }
    }

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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()
    }
    }

    View full-size slide

  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()
    }

    View full-size slide

  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()

    View full-size slide

  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)
    }

    View full-size slide

  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)
    }

    View full-size slide

  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)
    }

    View full-size slide

  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

    View full-size slide

  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
    ,

    View full-size slide

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

    View full-size slide

  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()

    View full-size slide

  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

    View full-size slide

  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)
    }

    View full-size slide

  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(...)

    View full-size slide

  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()
    }
    }

    View full-size slide

  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(...)

    View full-size slide

  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()

    View full-size slide

  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()
    }

    View full-size slide

  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()

    View full-size slide

  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

    View full-size slide

  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 {
    return scope.async {
    }
    }
    database.populate()

    View full-size slide

  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()

    View full-size slide

  123. Keeping track of coroutines
    Wouldn’t want to lose a Job

    View full-size slide

  124. Keeping track of coroutines

    runTest will wait for coroutines to complete if they are either:

    View full-size slide

  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)

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  129. Keeping track of coroutines
    class SocketService(private val ioDispatcher: CoroutineDispatcher) {
    private val scope = CoroutineScope(ioDispatcher)
    fun shutdown() {
    scope.launch {
    // Do some async cleanup
    }
    }
    }

    View full-size slide

  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
    }
    }

    View full-size slide

  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
    }
    }

    View full-size slide

  132. Handling the Main dispatcher
    A UI thread without a UI

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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 {

    View full-size slide

  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 {

    View full-size slide

  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()
    }
    }

    View full-size slide

  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!"
    }
    }

    View full-size slide

  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()

    View full-size slide

  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()

    View full-size slide

  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)
    }
    }

    View full-size slide

  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

    View full-size slide

  144. Managing test objects
    Creating dispatchers and schedulers and scopes

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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
    }
    }

    View full-size slide

  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)
    }
    }

    View full-size slide

  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
    }

    View full-size slide

  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())
    }
    }

    View full-size slide

  151. Using TestDispatchers outside the test

    View full-size slide

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

    View full-size slide

  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...
    }
    }

    View full-size slide

  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...
    }

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  161. Flows
    Suspenseful streams

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  164. Flows
    interface DataSource {
    fun counts(): Flow
    }
    class Repository(private val dataSource: DataSource) {
    fun scores(): Flow {
    return dataSource.counts().map { it * 10 }
    }
    }
    class ColdFakeDataSource : DataSource {
    override fun counts(): Flow {
    return flowOf(1, 2, 3, 4)
    }
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  167. 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)
    }

    View full-size slide

  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)
    val someValues = repository.scores().take(2).toList()
    assertEquals(10, someValues[0])
    assertEquals(20, someValues[1])
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  181. Flows
    @Test
    fun continuouslyCollect() = runTest {
    val dataSource = HotFakeDataSource()
    val repository = Repository(dataSource)
    val values = mutableListOf()
    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()

    View full-size slide

  182. Flows with Turbine

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  186. 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())
    }
    }

    View full-size slide

  187. StateFlows
    Look! It’s a stream! It’s a state holder!

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  192. StateFlows
    class ColdFakeRepository : MyRepository {
    override fun scores() = flow {
    emit(1)
    delay(100)
    emit(2)
    delay(100)
    emit(3)
    }
    }

    View full-size slide

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

    View full-size slide

  194. 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
    }
    }
    }

    View full-size slide

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

    View full-size slide

  196. 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)
    }

    View full-size slide

  197. 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)
    }

    View full-size slide

  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)
    advanceTimeBy(101)
    assertEquals(3, viewModel.data.value)
    }

    View full-size slide

  199. Summary
    What did we learn today?

    View full-size slide

  200. 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!)

    View full-size slide

  201. Resources
    ● goo.gle/coroutine-test-guide
    ● goo.gle/coroutine-test-migration
    ● github.com/cashapp/turbine
    NEW!

    View full-size slide

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

    View full-size slide