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

Untangling Coroutine Testing (Droidcon Berlin 2022)

Untangling Coroutine Testing (Droidcon Berlin 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.

For more, see
https://zsmb.co/talks/untangling-coroutine-testing/

Marton Braun

July 07, 2022
Tweet

More Decks by Marton Braun

Other Decks in Programming

Transcript

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

    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-16-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
    TestCoroutineScheduler
    TestScope
    TestDispatcher
    TestDispatcher
    All TestDispatchers must share the same scheduler
    TestDispatcher

    View Slide

  45. Test APIs
    TestDispatcher
    TestCoroutineScheduler

    View Slide

  46. Test APIs
    TestCoroutineScheduler
    StandardTestDispatcher UnconfinedTestDispatcher
    TestDispatcher

    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("Bob")
    reg("Alice")

    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("Bob")
    reg("Alice")

    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("Bob")
    reg("Alice")

    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

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

    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
    80
    ms
    90
    ms
    100
    ms
    500
    ms
    750
    ms

    runCurrent()

    advanceTimeBy(delayTimeMillis = 100)

    View Slide


  65. You need to advance those coroutines manually
    StandardTestDispatcher

    runCurrent()

    advanceTimeBy(delayTimeMillis = 100)
    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("Bob")
    reg("Alice")

    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. Idle
    StandardTestDispatcher
    @Test
    fun standardTest() = runTest {
    val repo = UserRepository()
    launch { repo.register("Alice") }
    launch { repo.register("Bob") }
    advanceUntilIdle()
    assertEquals(listOf("Alice", "Bob"), repo.getAllUsers())
    }
    UserRepo() reg("Alice")
    reg("Bob")

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

    View Slide

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

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

  80. UnconfinedTestDispatcher

    View Slide

  81. UnconfinedTestDispatcher

    Starts new coroutines eagerly (like runBlockingTest)

    View Slide

  82. UnconfinedTestDispatcher

    Starts new coroutines eagerly (like runBlockingTest)

    Can be a good choice for simple tests

    View Slide

  83. UnconfinedTestDispatcher

    Starts new coroutines eagerly (like runBlockingTest)

    Can be a good choice for simple tests

    Does not emulate real concurrency

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

    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())
    }
    this: TestScope

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

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

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

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

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

    View Slide

  93. TestDispatchers
    Queues up new coroutines
    Use by default
    Starts new coroutines eagerly
    Use selectively
    • While migrating tests from old APIs
    • As the Main dispatcher
    • For coroutines that collect values
    StandardTestDispatcher UnconfinedTestDispatcher

    View Slide

  94. Injecting dispatchers
    Something something heat exchangers

    View Slide

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

  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
    @Test
    fun repoTest() = runTest {
    val repository = Repository(FakeDatabase())
    repository.initialize()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }

    View Slide

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

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

  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
    db.populate()
    fun initialize() {
    scope.launch {
    database.populate()
    }
    }

    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.read()
    db.populate()
    suspend fun fetchData() =
    withContext(Dispatchers.IO) {
    database.read()
    }

    View Slide

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

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

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

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

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

  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)
    }
    fun initialize() {
    scope.launch {
    database.populate()
    }
    }
    Repo(...)

    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)
    }
    Repo(...)
    db.populate()
    fun initialize() {
    scope.launch {
    database.populate()
    }
    }

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

    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()
    suspend fun fetchData() =
    withContext(ioDispatcher) {
    database.read()
    }

    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)
    }
    Repo(...) db.populate() db.read() assert()

    View Slide

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

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

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

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

    View Slide

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

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

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

    View Slide

  128. Handling the Main dispatcher
    }
    val viewModel = HomeViewModel()
    viewModel.loadMessage()
    assertEquals("Greetings!", viewModel.message.value)
    @Test
    fun testGreeting() = runTest {
    java.lang.IllegalStateException: Module with the Main dispatcher
    had failed to initialize. For tests Dispatchers.setMain from
    kotlinx-coroutines-test module can be used

    View Slide

  129. Handling the Main dispatcher
    }
    val viewModel = HomeViewModel()
    viewModel.loadMessage()
    assertEquals("Greetings!", viewModel.message.value)
    @Test
    fun testGreeting() = runTest {
    java.lang.IllegalStateException: Module with the Main dispatcher
    had failed to initialize. For tests Dispatchers.setMain from
    kotlinx-coroutines-test module can be used

    View Slide

  130. Handling the Main dispatcher
    val testDispatcher = UnconfinedTestDispatcher(testScheduler)
    Dispatchers.setMain(testDispatcher)
    val viewModel = HomeViewModel()
    viewModel.loadMessage()
    assertEquals("Greetings!", viewModel.message.value)
    }
    @Test
    fun testGreeting() = runTest {
    java.lang.IllegalStateException: Module with the Main dispatcher
    had failed to initialize. For tests Dispatchers.setMain from
    kotlinx-coroutines-test module can be used

    View Slide

  131. Handling the Main dispatcher
    val testDispatcher = UnconfinedTestDispatcher(testScheduler)
    Dispatchers.setMain(testDispatcher)
    val viewModel = HomeViewModel()
    viewModel.loadMessage()
    assertEquals("Greetings!", viewModel.message.value)
    }
    @Test
    fun testGreeting() = runTest {
    java.lang.IllegalStateException: Module with the Main dispatcher
    had failed to initialize. For tests Dispatchers.setMain from
    kotlinx-coroutines-test module can be used

    View Slide

  132. java.lang.IllegalStateException: Module with the Main dispatcher
    had failed to initialize. For tests Dispatchers.setMain from
    kotlinx-coroutines-test module can be used
    Handling the Main dispatcher
    val testDispatcher = UnconfinedTestDispatcher(testScheduler)
    Dispatchers.setMain(testDispatcher)
    try {
    } finally {
    Dispatchers.resetMain()
    }
    val viewModel = HomeViewModel()
    viewModel.loadMessage()
    assertEquals("Greetings!", viewModel.message.value)
    }
    @Test
    fun testGreeting() = runTest {

    View Slide

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

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

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

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

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

  138. class HomeViewModelTestUsingRule {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()
    @Test
    fun testGreeting() = runTest {
    Handling the Main dispatcher
    val viewModel = HomeViewModel()
    viewModel.loadMessage()
    assertEquals("Greetings!", viewModel.message.value)
    }
    }

    View Slide

  139. Handling the Main dispatcher
    val viewModel = HomeViewModel()
    viewModel.loadMessage()
    assertEquals("Greetings!", viewModel.message.value)
    If you replace the Main dispatcher with a TestDispatcher,
    all new TestDispatchers will automatically share its scheduler
    class HomeViewModelTestUsingRule {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()
    @Test
    fun testGreeting() = runTest {
    }
    }

    View Slide

  140. Handling the Main dispatcher
    val unconfinedDispatcher = UnconfinedTestDispatcher()
    val standardDispatcher = StandardTestDispatcher()
    If you replace the Main dispatcher with a TestDispatcher,
    all new TestDispatchers will automatically share its scheduler
    class HomeViewModelTestUsingRule {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()
    @Test
    fun testGreeting() = runTest {
    }
    }

    View Slide

  141. Flows
    Suspenseful streams

    View Slide

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

    View Slide

  143. Flows
    Test Repository Fake Data Source

    View Slide

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

    View Slide

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

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

    View Slide

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

    View Slide

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

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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  160. Flows with Turbine

    View Slide

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

    View Slide

  162. Stream
    StateFlows
    State holder
    Source: Manuel Vivo

    View Slide

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

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

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

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

  167. StateFlows
    Test ViewModel Fake Repository

    View Slide

  168. StateFlows
    class HotFakeRepository : MyRepository {
    private val flow = MutableSharedFlow()
    suspend fun emit(value: Int) = flow.emit(value)
    override fun scores(): Flow = flow
    }

    View Slide

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

    View Slide

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

    View Slide

  171. StateFlows
    @Test
    fun testHotFakeRepository() = runTest {
    val fakeRepository = HotFakeRepository()
    val viewModel = MyViewModel(fakeRepository)
    assertEquals(0, viewModel.data.value)
    }

    View Slide

  172. StateFlows
    @Test
    fun testHotFakeRepository() = runTest {
    val fakeRepository = HotFakeRepository()
    val viewModel = MyViewModel(fakeRepository)
    assertEquals(0, viewModel.data.value)
    viewModel.initialize()
    }
    fun initialize() {
    viewModelScope.launch {
    myRepository.scores().collect { score ->
    _data.value = score
    }
    }
    }

    View Slide

  173. StateFlows
    @Test
    fun testHotFakeRepository() = runTest {
    val fakeRepository = HotFakeRepository()
    val viewModel = MyViewModel(fakeRepository)
    assertEquals(0, viewModel.data.value)
    viewModel.initialize()
    fakeRepository.emit(1)
    assertEquals(1, viewModel.data.value)
    }

    View Slide

  174. StateFlows
    @Test
    fun testHotFakeRepository() = runTest {
    val fakeRepository = HotFakeRepository()
    val viewModel = MyViewModel(fakeRepository)
    assertEquals(0, viewModel.data.value)
    viewModel.initialize()
    fakeRepository.emit(1)
    assertEquals(1, viewModel.data.value)
    fakeRepository.emit(2)
    fakeRepository.emit(3)
    assertEquals(3, viewModel.data.value)
    }

    View Slide

  175. StateFlows
    @Test
    fun testHotFakeRepository() = runTest {
    val fakeRepository = HotFakeRepository()
    val viewModel = MyViewModel(fakeRepository)
    assertEquals(0, viewModel.data.value)
    viewModel.initialize()
    fakeRepository.emit(1)
    assertEquals(1, viewModel.data.value)
    fakeRepository.emit(2)
    fakeRepository.emit(3)
    assertEquals(3, viewModel.data.value)
    }

    View Slide

  176. StateFlows
    @Test
    fun testHotFakeRepository() = runTest {
    val fakeRepository = HotFakeRepository()
    val viewModel = MyViewModel(fakeRepository)
    assertEquals(0, viewModel.data.value)
    viewModel.initialize()
    fakeRepository.emit(1)
    assertEquals(1, viewModel.data.value)
    fakeRepository.emit(2)
    fakeRepository.emit(3)
    assertEquals(3, viewModel.data.value)
    }

    View Slide

  177. StateFlows with stateIn
    private val _data = MutableStateFlow(0)
    _data.asStateFlow()
    fun initialize() {
    .launch {
    .collect { count ->
    _data.value = count
    }
    }
    }
    myRepository: MyRepository
    myRepository.scores()
    class MyViewModel(
    ) : ViewModel() {
    private val
    viewModelScope
    val data: StateFlow =
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  192. Summary
    What did we learn today?

    View Slide

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

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

    View Slide

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

    View Slide