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 Slide

  2. View Slide

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

    View Slide

  4. Testing suspending functions
    runTest. run.

    View Slide

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

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

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

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

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

  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 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)
    }
    import kotlinx.coroutines.test.runTest
    *

    View 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

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

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

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

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

  22. Testing new coroutines

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

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

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

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

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

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

    View Slide

  30. Test APIs
    runTest

    View Slide

  31. Test APIs
    runTest TestScope

    View Slide

  32. Test APIs
    runTest
    TestDispatcher
    TestScope

    View Slide

  33. Test APIs
    runTest
    TestDispatcher TestCoroutineScheduler
    TestScope

    View Slide

  34. Test APIs
    runTest
    TestDispatcher TestCoroutineScheduler
    TestScope
    TestDispatcher
    TestDispatcher

    View Slide

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

    View Slide

  36. Test APIs
    TestDispatcher
    TestCoroutineScheduler

    View Slide

  37. Test APIs
    TestCoroutineScheduler
    StandardTestDispatcher UnconfinedTestDispatcher
    TestDispatcher

    View Slide

  38. StandardTestDispatcher

    View Slide

  39. ● Queues up coroutines on the scheduler
    StandardTestDispatcher

    View Slide

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

    View Slide

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

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

    View Slide

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

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

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

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

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

    View Slide

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

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

  50. StandardTestDispatcher
    ● You need to advance those coroutines manually

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

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

  58. ● 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 Slide

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

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

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

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

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

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

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

  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

    View 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")
    Idle
    assert()

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

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

    View Slide

  71. UnconfinedTestDispatcher

    View Slide

  72. UnconfinedTestDispatcher
    ● Starts new coroutines eagerly

    View Slide

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

    View Slide

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

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

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

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

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

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

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

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

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

    View Slide

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

  85. Injecting dispatchers
    Something something heat exchangers

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

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

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

    View Slide

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

  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

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

    View Slide

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

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

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

  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 Slide

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

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

  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 Slide

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

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

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

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

    View Slide

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

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

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

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

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

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

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

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

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

    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)
    }
    fun initialize() {
    scope.launch {
    database.populate()
    }
    }
    This is a bad API, don’t do this

    View Slide

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

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

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

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

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

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

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

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

  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

    View Slide

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

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

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

    View Slide

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

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

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

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

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

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

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

  134. Flows
    Suspenseful streams

    View Slide

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

    View Slide

  136. Flows
    Test Repository Fake Data Source

    View Slide

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

    View Slide

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

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

    View Slide

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

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

    View Slide

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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

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

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

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

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

  155. View Slide

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

    View Slide

  157. Stream
    StateFlows
    State holder

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

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

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

  162. StateFlows
    Test ViewModel Fake Repository

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

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

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

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

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

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

    View Slide

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

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

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

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

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

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

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

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

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

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

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

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

  186. Summary
    What did we learn today?

    View Slide

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

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

    View Slide

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

    View Slide