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 Slide

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

    View Slide

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

    View Slide

  4. Introduction

    View Slide

  5. Introduction

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 Slide

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

    View Slide

  13. Testing suspending functions
    runTest. run.

    View Slide

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

    View 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 Slide

  31. Testing new coroutines

    View 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 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 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 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 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 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 Slide

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

    View Slide

  39. Test APIs
    runTest

    View Slide

  40. Test APIs
    runTest TestScope

    View Slide

  41. Test APIs
    runTest
    TestDispatcher
    TestScope

    View Slide

  42. Test APIs
    runTest
    TestDispatcher TestCoroutineScheduler
    TestScope

    View Slide

  43. Test APIs
    runTest
    TestDispatcher TestCoroutineScheduler
    TestScope
    TestDispatcher
    TestDispatcher

    View Slide

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

    View Slide

  45. Test APIs
    TestDispatcher
    TestCoroutineScheduler

    View Slide

  46. Test APIs
    TestDispatcher
    TestCoroutineScheduler
    StandardTestDispatcher UnconfinedTestDispatcher

    View Slide

  47. StandardTestDispatcher

    View Slide


  48. Queues up coroutines on the scheduler
    StandardTestDispatcher

    View Slide


  49. Queues up coroutines on the scheduler

    You need to advance those coroutines manually
    StandardTestDispatcher

    View 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 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 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 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 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 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 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 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 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 Slide


  59. You need to advance those coroutines manually
    StandardTestDispatcher

    View Slide


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

    View Slide


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

    runCurrent()

    View Slide


  62. You need to advance those coroutines manually
    StandardTestDispatcher

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

    View 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 Slide

  79. UnconfinedTestDispatcher

    View Slide

  80. UnconfinedTestDispatcher

    Starts new coroutines eagerly (like runBlockingTest)

    View Slide

  81. UnconfinedTestDispatcher

    Starts new coroutines eagerly (like runBlockingTest)

    Can be a good choice for simple tests

    View Slide

  82. UnconfinedTestDispatcher

    Starts new coroutines eagerly (like runBlockingTest)

    Can be a good choice for simple tests

    Does not emulate real concurrency

    View 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 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 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 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 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 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 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 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 Slide

  91. TestDispatchers

    View Slide

  92. TestDispatchers
    StandardTestDispatcher
    Queues up new coroutines
    Use by default

    View Slide

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

    View 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 Slide

  95. Injecting dispatchers
    Something something heat exchangers

    View 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 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 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 Slide

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

    View 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 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 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 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 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 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 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 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 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 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 Slide

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

    View 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 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 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 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 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 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 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 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 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 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 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 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 Slide

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

    View Slide

  124. Keeping track of coroutines

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

    View 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 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 Slide

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

    View Slide

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

    View 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 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 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 Slide

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

    View 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 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 Slide

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

    View 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 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 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 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 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 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 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 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 Slide

  144. Managing test objects
    Creating dispatchers and schedulers and scopes

    View Slide

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

    View Slide

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

    View 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 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 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 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 Slide

  151. Using TestDispatchers outside the test

    View 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 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 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 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 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  161. Flows
    Suspenseful streams

    View Slide

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

    View Slide

  163. Flows

    View Slide

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

    View Slide

  165. 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 Slide

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

    View Slide

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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 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 Slide

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

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

    View 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])
    }
    collectJob.cancel()

    View Slide

  182. 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 Slide

  183. Flows with Turbine

    View Slide

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

    View Slide

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

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

    View Slide

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

    View Slide

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

    View 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 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 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 Slide

  192. 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 Slide

  193. StateFlows

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

    View Slide

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

    View Slide

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

    View Slide

  201. Summary
    What did we learn today?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide