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

Untangling Coroutine Testing (KotlinConf '23)

Untangling Coroutine Testing (KotlinConf '23)

Coroutines are embraced on Android as a tool to perform asynchronous operations and manage threading in your apps. Testing them requires some extra work and a solid understanding of scopes and dispatchers.

In this talk, we’ll look at how to test coroutines with the latest available testing APIs introduced in kotlinx.coroutines 1.6, from the simplest cases all the way to Flows.

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

Marton Braun

April 13, 2023
Tweet

More Decks by Marton Braun

Other Decks in Programming

Transcript

  1. Márton Braun @zsmb13
    Developer Relations Engineer
    Google
    Untangling
    Coroutine
    Testing
    KotlinConf’23
    Amsterdam

    View full-size slide

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

    View full-size slide

  3. Testing suspending functions
    runTest. run.

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  12. Testing Dispatcher changes
    suspend fun fetchData(): String = withContext(Dispatchers.IO) {
    delay(1000L)
    "Hello world"
    }
    @Test
    fun dataIsHelloWorld() = runTest {
    val data = fetchData()
    assertEquals("Hello world", data)
    }
    import kotlinx.coroutines.test.runTest

    View full-size slide

  13. Testing Dispatcher changes
    suspend fun fetchData(): String = withContext(Dispatchers.IO) {
    delay(1000L)
    "Hello world"
    }
    @Test
    fun dataIsHelloWorld() = runTest {
    val data = fetchData()
    assertEquals("Hello world", data)
    }
    import kotlinx.coroutines.test.runTest
    *

    View full-size slide

  14. Testing Dispatcher changes
    suspend fun fetchData(): String = withContext(Dispatchers.IO) {
    delay(1000L)
    "Hello world"
    }
    @Test
    fun dataIsHelloWorld() = runTest {
    val data = fetchData()
    assertEquals("Hello world", data)
    }
    Test thread
    Dispatchers.IO

    View full-size slide

  15. Testing Dispatcher changes
    suspend fun fetchData(): String = withContext(Dispatchers.IO) {
    delay(1000L)
    "Hello world"
    }
    @Test
    fun dataIsHelloWorld() = runTest {
    val data = fetchData()
    assertEquals("Hello world", data)
    }
    Test thread
    Dispatchers.IO
    fetchData()

    View full-size slide

  16. Testing Dispatcher changes
    suspend fun fetchData(): String = withContext(Dispatchers.IO) {
    delay(1000L)
    "Hello world"
    }
    @Test
    fun dataIsHelloWorld() = runTest {
    val data = fetchData()
    assertEquals("Hello world", data)
    }
    Test thread
    Dispatchers.IO
    fetchData()

    View full-size slide

  17. Testing Dispatcher changes
    suspend fun fetchData(): String = withContext(Dispatchers.IO) {
    delay(1000L)
    "Hello world"
    }
    @Test
    fun dataIsHelloWorld() = runTest {
    val data = fetchData()
    assertEquals("Hello world", data)
    }
    Test thread
    Dispatchers.IO
    fetchData()
    delay()

    View full-size slide

  18. Testing Dispatcher changes
    suspend fun fetchData(): String = withContext(Dispatchers.IO) {
    delay(1000L)
    "Hello world"
    }
    @Test
    fun dataIsHelloWorld() = runTest {
    val data = fetchData()
    assertEquals("Hello world", data)
    }
    Test thread
    Dispatchers.IO
    fetchData()
    delay()

    View full-size slide

  19. Testing Dispatcher changes
    suspend fun fetchData(): String = withContext(Dispatchers.IO) {
    delay(1000L)
    "Hello world"
    }
    @Test
    fun dataIsHelloWorld() = runTest {
    val data = fetchData()
    assertEquals("Hello world", data)
    }
    Test thread
    Dispatchers.IO
    fetchData()
    delay()
    assert()

    View full-size slide

  20. Testing Dispatcher changes
    suspend fun fetchData(): String = withContext(Dispatchers.IO) {
    delay(1000L)
    "Hello world"
    }
    @Test
    fun dataIsHelloWorld() = runTest {
    val data = fetchData()
    assertEquals("Hello world", data)
    }
    Test thread
    Dispatchers.IO
    fetchData()
    delay()
    assert()

    View full-size slide

  21. Testing new coroutines

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  24. Testing new coroutines
    class ProfileViewModel : ViewModel() {
    lateinit var user: User
    fun initialize() {
    viewModelScope.launch {
    user = fetchUser()
    }
    }
    }
    @Test
    fun directExample() = runTest {
    val repo = UserRepository()
    launch { repo.register("Alice") }
    launch { repo.register("Bob") }
    assertEquals(
    listOf("Alice", "Bob"),
    repo.getAllUsers()
    )
    }

    View full-size slide

  25. Testing new coroutines
    class ProfileViewModel : ViewModel() {
    lateinit var user: User
    fun initialize() {
    viewModelScope.launch {
    user = fetchUser()
    }
    }
    }
    @Test
    fun indirectExample() = runTest {
    val viewModel = ProfileViewModel()
    viewModel.initialize()
    assertEquals("Sam", viewModel.user.name)
    }
    @Test
    fun directExample() = runTest {
    val repo = UserRepository()
    launch { repo.register("Alice") }
    launch { repo.register("Bob") }
    assertEquals(
    listOf("Alice", "Bob"),
    repo.getAllUsers()
    )
    }

    View full-size slide

  26. Testing new coroutines
    class ProfileViewModel : ViewModel() {
    lateinit var user: User
    fun initialize() {
    viewModelScope.launch {
    user = fetchUser()
    }
    }
    }
    @Test
    fun indirectExample() = runTest {
    val viewModel = ProfileViewModel()
    viewModel.initialize()
    assertEquals("Sam", viewModel.user.name)
    }
    @Test
    fun directExample() = runTest {
    val repo = UserRepository()
    launch { repo.register("Alice") }
    launch { repo.register("Bob") }
    assertEquals(
    listOf("Alice", "Bob"),
    repo.getAllUsers()
    )
    }

    View full-size slide

  27. Testing new coroutines
    class ProfileViewModel : ViewModel() {
    lateinit var user: User
    fun initialize() {
    viewModelScope.launch {
    user = fetchUser()
    }
    }
    }
    @Test
    fun indirectExample() = runTest {
    val viewModel = ProfileViewModel()
    viewModel.initialize()
    assertEquals("Sam", viewModel.user.name)
    }
    @Test
    fun directExample() = runTest {
    val repo = UserRepository()
    launch { repo.register("Alice") }
    launch { repo.register("Bob") }
    assertEquals(
    listOf("Alice", "Bob"),
    repo.getAllUsers()
    )
    }

    View full-size slide

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

    View full-size slide

  29. Test APIs
    runTest

    View full-size slide

  30. Test APIs
    runTest TestScope

    View full-size slide

  31. Test APIs
    runTest
    TestDispatcher
    TestScope

    View full-size slide

  32. Test APIs
    runTest
    TestDispatcher TestCoroutineScheduler
    TestScope

    View full-size slide

  33. Test APIs
    runTest
    TestDispatcher TestCoroutineScheduler
    TestScope
    TestDispatcher
    TestDispatcher

    View full-size slide

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

    View full-size slide

  35. Test APIs
    TestDispatcher
    TestCoroutineScheduler

    View full-size slide

  36. Test APIs
    TestCoroutineScheduler
    StandardTestDispatcher UnconfinedTestDispatcher
    TestDispatcher

    View full-size slide

  37. StandardTestDispatcher

    View full-size slide

  38. ● Queues up coroutines on the scheduler
    StandardTestDispatcher

    View full-size slide

  39. ● Queues up coroutines on the scheduler
    ● You need to advance those coroutines manually
    StandardTestDispatcher

    View full-size slide

  40. ● Queues up coroutines on the scheduler
    ● You need to advance those coroutines manually

    runTest uses a StandardTestDispatcher by default
    StandardTestDispatcher
    runTest
    StandardTestDispatcher
    TestScope

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  46. 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 full-size slide

  47. 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 full-size slide

  48. 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 full-size slide

  49. StandardTestDispatcher
    ● You need to advance those coroutines manually

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  54. StandardTestDispatcher
    now now
    80
    ms
    90
    ms
    100
    ms
    500
    ms
    750
    ms
    ● runCurrent()
    ● advanceTimeBy(delayTimeMillis = 100)
    ● You need to advance those coroutines manually

    View full-size slide

  55. StandardTestDispatcher
    ● runCurrent()
    ● advanceTimeBy(delayTimeMillis = 100)
    now now
    80
    ms
    90
    ms
    100
    ms
    500
    ms
    750
    ms
    ● You need to advance those coroutines manually

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  65. 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 full-size slide

  66. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  70. UnconfinedTestDispatcher

    View full-size slide

  71. UnconfinedTestDispatcher
    ● Starts new coroutines eagerly

    View full-size slide

  72. UnconfinedTestDispatcher
    ● Starts new coroutines eagerly
    ● Can be a good choice for simple tests

    View full-size slide

  73. UnconfinedTestDispatcher
    ● Starts new coroutines eagerly
    ● Can be a good choice for simple tests
    ● Does not emulate real concurrency

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  83. TestDispatchers
    Queues up new coroutines
    Use by default
    Starts new coroutines eagerly
    Use selectively
    • As the Main dispatcher
    • For coroutines that collect Flows
    StandardTestDispatcher UnconfinedTestDispatcher

    View full-size slide

  84. Injecting dispatchers
    Something something heat exchangers

    View full-size slide

  85. Injecting dispatchers
    class Repository(private val database: Database) {
    private val scope = CoroutineScope(Dispatchers.IO)
    fun initialize() {
    scope.launch {
    database.populate()
    }
    }
    suspend fun fetchData(): String = withContext(Dispatchers.IO) {
    database.read()
    }
    }

    View full-size slide

  86. Injecting dispatchers
    class Repository(private val database: Database) {
    private val scope = CoroutineScope(Dispatchers.IO)
    fun initialize() {
    scope.launch {
    database.populate()
    }
    }
    suspend fun fetchData(): String = withContext(Dispatchers.IO) {
    database.read()
    }
    }

    View full-size slide

  87. Injecting dispatchers
    class Repository(private val database: Database) {
    private val scope = CoroutineScope(Dispatchers.IO)
    fun initialize() {
    scope.launch {
    database.populate()
    }
    }
    suspend fun fetchData(): String = withContext(Dispatchers.IO) {
    database.read()
    }
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  90. Injecting dispatchers
    @Test
    fun repoTest() = runTest {
    val repository = Repository(FakeDatabase())
    repository.initialize()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }
    Repo(FakeDb())
    Test thread
    Dispatchers.IO

    View full-size slide

  91. Injecting dispatchers
    @Test
    fun repoTest() = runTest {
    val repository = Repository(FakeDatabase())
    repository.initialize()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }
    Repo(FakeDb())
    Test thread
    Dispatchers.IO
    db.populate()
    fun initialize() {
    scope.launch {
    database.populate()
    }
    }

    View full-size slide

  92. Injecting dispatchers
    @Test
    fun repoTest() = runTest {
    val repository = Repository(FakeDatabase())
    repository.initialize()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }
    Repo(FakeDb())
    Test thread
    Dispatchers.IO db.read()
    db.populate()
    suspend fun fetchData() =
    withContext(Dispatchers.IO) {
    database.read()
    }

    View full-size slide

  93. assert()
    Injecting dispatchers
    @Test
    fun repoTest() = runTest {
    val repository = Repository(FakeDatabase())
    repository.initialize()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }
    Repo(FakeDb())
    Test thread
    Dispatchers.IO db.read()
    db.populate()

    View full-size slide

  94. assert()
    Injecting dispatchers
    Repo(FakeDb())
    Test thread
    Dispatchers.IO db.read()
    db.populate()
    @Test
    fun repoTest() = runTest {
    val repository = Repository(FakeDatabase())
    repository.initialize()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }

    View full-size slide

  95. Injecting dispatchers
    Repo(FakeDb())
    Test thread
    Dispatchers.IO db.read()
    db.populate()
    assert()
    @Test
    fun repoTest() = runTest {
    val repository = Repository(FakeDatabase())
    repository.initialize()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }

    View full-size slide

  96. Injecting dispatchers
    Repo(FakeDb())
    Test thread
    Dispatchers.IO db.read()
    db.populate()
    assert()
    @Test
    fun repoTest() = runTest {
    val repository = Repository(FakeDatabase())
    repository.initialize()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }

    View full-size slide

  97. ,
    ioDispatcher
    ioDispatcher
    Injecting dispatchers
    class Repository(
    Dispatchers.IO)
    fun initialize() {
    scope.launch {
    database.populate()
    }
    }
    suspend fun fetchData(): String = withContext( ) {
    database.read()
    }
    }
    private val database: Database) {
    private val scope = CoroutineScope(
    Dispatchers.IO

    View full-size slide

  98. Dispatchers.IO
    Injecting dispatchers
    class Repository(
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
    ioDispatcher)
    fun initialize() {
    scope.launch {
    database.populate()
    }
    }
    suspend fun fetchData(): String = withContext( ) {
    database.read()
    }
    }
    private val database: Database
    ) {
    private val scope = CoroutineScope(
    ioDispatcher
    ,

    View full-size slide

  99. Dispatchers.IO
    Injecting dispatchers
    class Repository(
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
    ioDispatcher)
    fun initialize() {
    scope.launch {
    database.populate()
    }
    }
    suspend fun fetchData(): String = withContext( ) {
    database.read()
    }
    }
    private val database: Database
    ) {
    private val scope = CoroutineScope(
    ioDispatcher
    ,

    View full-size slide

  100. Dispatchers.IO
    Injecting dispatchers
    class Repository(
    private val ioDispatcher: CoroutineContext = Dispatchers.IO,
    ioDispatcher)
    fun initialize() {
    scope.launch {
    database.populate()
    }
    }
    suspend fun fetchData(): String = withContext( ) {
    database.read()
    }
    }
    private val database: Database
    ) {
    private val scope = CoroutineScope(
    ioDispatcher
    ,

    View full-size slide

  101. Dispatchers.IO
    Injecting dispatchers
    class Repository(
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
    ioDispatcher)
    fun initialize() {
    scope.launch {
    database.populate()
    }
    }
    suspend fun fetchData(): String = withContext( ) {
    database.read()
    }
    }
    private val database: Database
    ) {
    private val scope = CoroutineScope(
    ioDispatcher
    ,

    View full-size slide

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

    View full-size slide

  103. Injecting dispatchers
    @Test
    fun repoTest() = runTest {
    val repository = Repository(
    database = ,
    ioDispatcher = StandardTestDispatcher(testScheduler),
    )
    repository.initialize()
    advanceUntilIdle()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }
    FakeDatabase()

    View full-size slide

  104. Injecting dispatchers
    @Test
    fun repoTest() = runTest {
    val repository = Repository(
    database = FakeDatabase(),
    ioDispatcher = StandardTestDispatcher(testScheduler),
    )
    repository.initialize()
    advanceUntilIdle()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }
    this: TestScope

    View full-size slide

  105. Injecting dispatchers
    @Test
    fun repoTest() = runTest {
    val repository = Repository(
    database = FakeDatabase(),
    ioDispatcher = StandardTestDispatcher(testScheduler),
    )
    repository.initialize()
    advanceUntilIdle()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }

    View full-size slide

  106. Injecting dispatchers
    @Test
    fun repoTest() = runTest {
    val repository = Repository(
    database = FakeDatabase(),
    ioDispatcher = StandardTestDispatcher(testScheduler),
    )
    repository.initialize()
    advanceUntilIdle()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }
    Repo(...)

    View full-size slide

  107. 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 full-size slide

  108. Injecting dispatchers
    @Test
    fun repoTest() = runTest {
    val repository = Repository(
    database = FakeDatabase(),
    ioDispatcher = StandardTestDispatcher(testScheduler),
    )
    repository.initialize()
    advanceUntilIdle()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }
    Repo(...)
    db.populate()
    fun initialize() {
    scope.launch {
    database.populate()
    }
    }

    View full-size slide

  109. Injecting dispatchers
    @Test
    fun repoTest() = runTest {
    val repository = Repository(
    database = FakeDatabase(),
    ioDispatcher = StandardTestDispatcher(testScheduler),
    )
    repository.initialize()
    advanceUntilIdle()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }
    Repo(...)
    db.populate()

    View full-size slide

  110. Injecting dispatchers
    @Test
    fun repoTest() = runTest {
    val repository = Repository(
    database = FakeDatabase(),
    ioDispatcher = StandardTestDispatcher(testScheduler),
    )
    repository.initialize()
    advanceUntilIdle()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }
    Repo(...) db.populate()

    View full-size slide

  111. Injecting dispatchers
    @Test
    fun repoTest() = runTest {
    val repository = Repository(
    database = FakeDatabase(),
    ioDispatcher = StandardTestDispatcher(testScheduler),
    )
    repository.initialize()
    advanceUntilIdle()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }
    Repo(...) db.populate() db.read()
    suspend fun fetchData() =
    withContext(ioDispatcher) {
    database.read()
    }

    View full-size slide

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

    View full-size slide

  113. Injecting dispatchers
    @Test
    fun repoTest() = runTest {
    val repository = Repository(
    database = FakeDatabase(),
    ioDispatcher = StandardTestDispatcher(testScheduler),
    )
    repository.initialize()
    advanceUntilIdle()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }
    fun initialize() {
    scope.launch {
    database.populate()
    }
    }
    This is a bad API, don’t do this

    View full-size slide

  114. Injecting dispatchers
    @Test
    fun repoTest() = runTest {
    val repository = Repository(
    database = FakeDatabase(),
    ioDispatcher = StandardTestDispatcher(testScheduler),
    )
    repository.initialize().await().
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }
    fun initialize(): Deferred {
    return scope.async {
    }
    }
    database.populate()

    View full-size slide

  115. suspend fun initialize() {
    }
    Injecting dispatchers
    @Test
    fun repoTest() = runTest {
    val repository = Repository(
    database = FakeDatabase(),
    ioDispatcher = StandardTestDispatcher(testScheduler),
    )
    repository.initialize()
    val data = repository.fetchData()
    assertEquals("Hello world", data)
    }
    database.populate()

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  120. 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 full-size slide

  121. 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 full-size slide

  122. 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 full-size slide

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

    View full-size slide

  124. 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 full-size slide

  125. Handling the Main dispatcher
    @Test
    fun testGreeting() = runTest {
    val testDispatcher = UnconfinedTestDispatcher(testScheduler)
    Dispatchers.setMain(testDispatcher)
    try {
    val viewModel = HomeViewModel()
    viewModel.loadMessage()
    assertEquals("Greetings!", viewModel.message.value)
    } finally {
    Dispatchers.resetMain()
    }
    }

    View full-size slide

  126. Handling the Main dispatcher
    @Test
    fun testGreeting() = runTest {
    val testDispatcher = UnconfinedTestDispatcher(testScheduler)
    Dispatchers.setMain(testDispatcher)
    try {
    val viewModel = HomeViewModel()
    viewModel.loadMessage()
    assertEquals("Greetings!", viewModel.message.value)
    } finally {
    Dispatchers.resetMain()
    }
    } fun loadMessage() {
    viewModelScope.launch {
    _message.value = "Greetings!"
    }
    }

    View full-size slide

  127. Handling the Main dispatcher
    @Test
    fun testGreeting() = runTest {
    val testDispatcher = UnconfinedTestDispatcher(testScheduler)
    try {
    val viewModel = HomeViewModel()
    viewModel.loadMessage()
    assertEquals("Greetings!", viewModel.message.value)
    } finally {
    }
    }
    Dispatchers.setMain(testDispatcher)
    Dispatchers.resetMain()

    View full-size slide

  128. Handling the Main dispatcher
    class MainDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
    ) : TestWatcher() {
    override fun starting(description: Description) {
    }
    override fun finished(description: Description) {
    }
    }
    Dispatchers.setMain(testDispatcher)
    Dispatchers.resetMain()

    View full-size slide

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

    View full-size slide

  130. 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 full-size slide

  131. 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 full-size slide

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

    View full-size slide

  133. Flows
    Suspenseful streams

    View full-size slide

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

    View full-size slide

  135. Flows
    Test Repository Fake Data Source

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  140. Flows
    @Test
    fun useTerminalOperators() = runTest {
    val repository = Repository(ColdFakeDataSource())
    val first = repository.scores().first()
    assertEquals(10, first)
    val values = repository.scores().toList()
    assertEquals(10, values[0])
    assertEquals(20, values[1])
    assertEquals(4, values.size)
    }

    View full-size slide

  141. Flows
    @Test
    fun useTerminalOperators() = runTest {
    val repository = Repository(ColdFakeDataSource())
    val first = repository.scores().first()
    assertEquals(10, first)
    val values = repository.scores().toList()
    assertEquals(10, values[0])
    assertEquals(20, values[1])
    assertEquals(4, values.size)
    val someValues = repository.scores().take(2).toList()
    assertEquals(10, someValues[0])
    assertEquals(20, someValues[1])
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  149. 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 full-size slide

  150. 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 full-size slide

  151. Flows
    val collectJob =
    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)
    launch(UnconfinedTestDispatcher()) {
    }
    }

    View full-size slide

  152. Flows
    @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)
    this: TestScope
    backgroundScope.launch(UnconfinedTestDispatcher()) {
    }
    }

    View full-size slide

  153. backgroundScope.launch(UnconfinedTestDispatcher()) {
    repository.scores().toList(values)
    }
    dataSource.emit(1)
    assertEquals(10, values[0])
    dataSource.emit(2)
    dataSource.emit(3)
    assertEquals(30, values.last())
    assertEquals(3, values.size)
    }
    @Test
    fun continuouslyCollect() = runTest {
    val dataSource = HotFakeDataSource()
    val repository = Repository(dataSource)
    val values = mutableListOf()
    Flows

    View full-size slide

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

    View full-size slide

  155. Stream
    StateFlows
    State holder

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  160. StateFlows
    Test ViewModel Fake Repository

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  165. 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 full-size slide

  166. 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 full-size slide

  167. 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 full-size slide

  168. 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 full-size slide

  169. 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 full-size slide

  170. 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 full-size slide

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

    View full-size slide

  172. 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 full-size slide

  173. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  180. 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 full-size slide

  181. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  184. Summary
    What did we learn today?

    View full-size slide

  185. Summary
    ● Use runTest for tests with coroutines
    ● Inject dispatchers into your classes to make them testable
    ● Create TestDispatchers as needed, always share a single scheduler
    ● Replace the Main dispatcher in unit tests (also simplifies sharing!)
    ● Use backgroundScope to create Flow collectors

    View full-size slide

  186. Resources
    ● goo.gle/coroutine-test-guide
    ● goo.gle/flow-test-guide
    ● github.com/cashapp/turbine

    View full-size slide

  187. Thank you, and
    don’t forget
    to vote!
    KotlinConf’23
    Amsterdam
    Márton Braun @zsmb13
    Developer Relations Engineer
    Google

    View full-size slide