Unit Testing Kotlin Channels & Flows - Android Summit

B3f560d34c14a9113e5024bc34ac26a0?s=47 Mohit S
October 09, 2020

Unit Testing Kotlin Channels & Flows - Android Summit

Unit testing Channels and Flows can be a challenge as they are fairly new. In this talk, I will share with you how to implement and test practical examples from my experience. These examples are testing delays, retries, and errors. I'll also share testing more complex examples such as polling. For each use case, we'll look at how to use features in the coroutines library such as runBlockingTest and TestCoroutineDispatcher. From my journey of using and testing Flows in production, I'll share the challenges I experienced.

B3f560d34c14a9113e5024bc34ac26a0?s=128

Mohit S

October 09, 2020
Tweet

Transcript

  1. Mohit Sarveiya Unit Testing Kotlin Channels & Flows www.twitter.com/heyitsmohit www.codingwithmohit.com

  2. Unit Testing Channels & Flows • Run Blocking Test •

    Flow Test Cases • Flow Assertions with Turbine • View Model Testing • Testing in Coroutines Lib
  3. Flow Test Cases

  4. Flow Test Cases • Collection • Run on Dispatcher •

    Errors • Retries Fail • Retry Succeeds
  5. None
  6. None
  7. GraphQL gRPC REST

  8. None
  9. Flow of users

  10. Flow of users Repository Service

  11. class UsersRepository(val apiService: ApiService) { fun getUsers(): Flow<String> { return

    apiService.getUsers() .filter { it.location "== "NYC" } .map { it.name } } }
  12. class UsersRepository(val apiService: ApiService) { fun getUsers(): Flow<String> { return

    apiService.getUsers() .filter { it.location "== "NYC" } .map { it.name } } }
  13. class UsersRepository(val apiService: ApiService) { fun getUsers(): Flow<String> { return

    apiService.getUsers() .filter { it.location "== "NYC" } .map { it.name } } }
  14. class UsersRepository(val apiService: ApiService) { fun getUsers(): Flow<String> { return

    apiService.getUsers() .filter { it.location "== "NYC" } .map { it.name } } } Flow of Users
  15. class UsersRepository(val apiService: ApiService) { fun getUsers(): Flow<String> { return

    apiService.getUsers() .filter { it.location "== "NYC" } .map { it.name } } } Filter users
  16. class UsersRepository(val apiService: ApiService) { fun getUsers(): Flow<String> { return

    apiService.getUsers() .filter { it.location "== "NYC" } .map { it.name } } } Map user name
  17. class UsersRepository(val apiService: ApiService) { fun getUsers(): Flow<String> { return

    apiService.getUsers() .filter { it.location "== "NYC" } .map { it.name } } } How do we test this Flow?
  18. Kotlin/kotlinx.coroutines Code ! Issues Pull Requests kotlinx-coroutines-core 2,093 commits 4c28f942

    13 hours ago Kotlinx-coroutines-debug Kotlinx-coroutines-test reactive js benchmarks
  19. Kotlin/kotlinx.coroutines Code ! Issues Pull Requests kotlinx-coroutines-core 2,093 commits 4c28f942

    13 hours ago Kotlinx-coroutines-debug Kotlinx-coroutines-test reactive js benchmarks Testing Module
  20. Kotlin/kotlinx.coroutines Code ! Issues Pull Requests README.md Module Kotlin-coroutines-test Test

    utilities for kotlinx.coroutines dependencies { testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9' }
  21. @Test fun `should get users`() { }

  22. @Test fun `should get users`() = runBlockingTest { } Creates

    a coroutine
  23. @Test fun `should get users`() = runBlockingTest { } Test

    Scope
  24. @Test fun `should get users`() = runBlockingTest { } Test

    Scope + Test Dispatcher
  25. @Test fun `should get users`() = runBlockingTest { } Test

    Scope + Test Dispatcher What is executed in this coroutine?
  26. @Test fun `should get users`() = runBlockingTest { } Test

    Scope + Test Dispatcher
  27. @Test fun `should get users`() = runBlockingTest { } Test

    Scope + Test Dispatcher
  28. fun runBlockingTest(testBody) { val scope = TestCoroutineScope(safeContext) val deferred =

    scope.async { scope.testBody() } deferred.getCompletionExceptionOrNull()"?.let { throw it } scope.cleanupTestCoroutines() if (hasActiveJobs()) { throw UncompletedCoroutinesError("Test finished with active jobs: $endingJobs") } } Our test to run
  29. fun runBlockingTest(testBody) { val scope = TestCoroutineScope(safeContext) val deferred =

    scope.async { scope.testBody() } deferred.getCompletionExceptionOrNull()"?.let { throw it } scope.cleanupTestCoroutines() if (hasActiveJobs()) { throw UncompletedCoroutinesError("Test finished with active jobs: $endingJobs") } } Test Coroutine Scope
  30. fun runBlockingTest(testBody) { val scope = TestCoroutineScope(safeContext) val deferred =

    scope.async { scope.testBody() } deferred.getCompletionExceptionOrNull()"?.let { throw it } scope.cleanupTestCoroutines() if (hasActiveJobs()) { throw UncompletedCoroutinesError("Test finished with active jobs: $endingJobs") } } Wrap your test in Deferred
  31. fun runBlockingTest(testBody) { val scope = TestCoroutineScope(safeContext) val deferred =

    scope.async { scope.testBody() } deferred.getCompletionExceptionOrNull()"?.let { throw it } scope.cleanupTestCoroutines() if (hasActiveJobs()) { throw UncompletedCoroutinesError("Test finished with active jobs: $endingJobs") } } Run your test!
  32. fun runBlockingTest(testBody) { val scope = TestCoroutineScope(safeContext) val deferred =

    scope.async { scope.testBody() } deferred.getCompletionExceptionOrNull()"?.let { throw it } scope.cleanupTestCoroutines() if (hasActiveJobs()) { throw UncompletedCoroutinesError("Test finished with active jobs: $endingJobs") } } Cancel all Jobs in test
  33. fun runBlockingTest(testBody) { val scope = TestCoroutineScope(safeContext) val deferred =

    scope.async { scope.testBody() } deferred.getCompletionExceptionOrNull()"?.let { throw it } scope.cleanupTestCoroutines() if (hasActiveJobs()) { throw UncompletedCoroutinesError("Test finished with active jobs: $endingJobs") } } Fail on active coroutines
  34. fun runBlockingTest(testBody) { val scope = TestCoroutineScope(safeContext) val deferred =

    scope.async { scope.testBody() } deferred.getCompletionExceptionOrNull()"?.let { throw it } scope.cleanupTestCoroutines() if (activeJobs()).isNotEmpty()) { throw UncompletedCoroutinesError("Test finished with active jobs") } } Setup Run Test Cleanup
  35. @Test fun `should get users`() = runBlockingTest { }

  36. class UsersRepository(val apiService: ApiService) { fun getUsers(): Flow<String> { return

    apiService.getUsers() .filter { it.location "== "NYC" } .map { it.name } } } How do we mock service?
  37. class UsersRepository(val apiService: ApiService) { fun getUsers(): Flow<String> { return

    apiService.getUsers() .filter { it.location "== "NYC" } .map { it.name } } } Mock or Create fake
  38. nhaarman/mockito-kotlin Code ! Issues Pull Requests Mockito-Kotlin A small library

    that provides helper functions to work with Mockito in Kotlin. maven central 2.2.0
  39. val apiService = mock<ApiService> { on { getUsers() } doReturn

    users.asFlow() } @Test fun `should get users`() = runBlockingTest { } Mock API Service
  40. val apiService = mock<ApiService> { on { getUsers() } doReturn

    users.asFlow() } @Test fun `should get users`() = runBlockingTest { } Return a Flow of users
  41. val apiService = mock<ApiService> { on { getUsers() } doReturn

    users.asFlow() } @Test fun `should get users`() = runBlockingTest { } val users = listOf( User(1, "User 1", "NJ"), User(2, "User 2", "NYC"), User(3, "User 3", "CA"), User(4, "User 4", "NJ"), User(5, "User 5", "NYC") )
  42. val apiService = mock<ApiService> { on { getUsers() } doReturn

    users.asFlow() } val usersRepository = UsersRepository(apiService) fun `should get users`() = runBlockingTest { } Inject mock into Repo
  43. @Test fun `should get users`() = runBlockingTest { val users

    = usersRepository.getUsers().toList() assertEquals(users, listOf("User 2", "User 5")) } Collect Flow into List
  44. @Test fun `should get users`() = runBlockingTest { val users

    = usersRepository.getUsers().toList() assertEquals(users, listOf("User 2", "User 5")) } Assert on returned user names
  45. @Test fun `should get users`() = runBlockingTest { val users

    = usersRepository.getUsers().toList() assertEquals(users, listOf("User 2", "User 5")) } Is there a better way to collect from Flows in test?
  46. cashApp/turbine Code ! Issues Pull Requests Turbine Small testing library

    for kotlinx.coroutines Flow. testImplementation 'app.cash.turbine:turbine:0.2.1'
  47. @Test fun `should get users`() = runBlockingTest { val users

    = usersRepository.getUsers().test { expectItem() shouldBe "User 2" expectItem() shouldBe "User 5" expectComplete() } } Library has test extension on Flow
  48. @Test fun `should get users`() = runBlockingTest { val users

    = usersRepository.getUsers().test { expectItem() shouldBe "User 2" expectItem() shouldBe "User 5" expectComplete() } } Read from item from Flow
  49. @Test fun `should get users`() = runBlockingTest { val users

    = usersRepository.getUsers().test { expectItem() shouldBe "User 2" expectItem() shouldBe "User 5" expectComplete() } } Assertion
  50. @Test fun `should get users`() = runBlockingTest { val users

    = usersRepository.getUsers().test { expectItem() shouldBe "User 2" expectItem() shouldBe "User 5" expectComplete() } } Use expectItem to fetch each item
  51. @Test fun `should get users`() = runBlockingTest { val users

    = usersRepository.getUsers().test { expectItem() shouldBe "User 2" expectItem() shouldBe "User 5" expectComplete() } } Verify no more emission by Flow
  52. @Test fun `should get users`() = runBlockingTest { val users

    = usersRepository.getUsers().test { expectItem() shouldBe "User 2" expectItem() shouldBe "User 5" expectComplete() } } What if flow emitted more items than 2?
  53. @Test fun `should get users`() = runBlockingTest { val users

    = usersRepository.getUsers().test { expectItem() shouldBe "User 2" expectItem() shouldBe "User 5" expectComplete() } } UserRepoTests.kt Run: UserRepoTest should get users app.cash.turbine.AssertionError: Expected complete but found Item(User 6) One more item in Flow
  54. @Test fun `should get users`() = runBlockingTest { val users

    = usersRepository.getUsers().test { expectItem() shouldBe "User 2" expectItem() shouldBe "User 5" expectItem() shouldBe "User 6” expectComplete() } } UserRepoTests.kt Run: UserRepoTest should get users Add expect item
  55. @Test fun `should get users`() = runBlockingTest { val users

    = usersRepository.getUsers().test { expectItem() shouldBe "User 2" expectItem() shouldBe "User 5" expectComplete() } } What if we didn’t expect complete?
  56. @Test fun `should get users`() = runBlockingTest { val users

    = usersRepository.getUsers().test { expectItem() shouldBe "User 2" expectItem() shouldBe "User 5" } } UserRepoTests.kt Run: UserRepoTest should get users app.cash.turbine.AssertionError: Unconsumed events found: - Complete Need to assert flow finished
  57. @Test fun `should get users`() = runBlockingTest { val users

    = usersRepository.getUsers().test { expectItem() shouldBe "User 2" expectItem() shouldBe "User 5" expectComplete() } } UserRepoTests.kt Run: should get users UserRepoTest
  58. @Test fun `should get users`() = runBlockingTest { val users

    = usersRepository.getUsers().test { expectItem() shouldBe "User 2" expectItem() shouldBe "User 5" expectComplete() } } UserRepoTests.kt Run: should get users UserRepoTest How does this work?
  59. Flow to test How Turbine Works

  60. Channel (Unlimited) How Turbine Works

  61. Channel (Unlimited) sealed class Event<out T> { object Complete data

    class Error(""...) data class Item(val value: T) } Event How Turbine Works
  62. Channel (Unlimited) data class Item(val value: T): Event() How Turbine

    Works
  63. Channel (Unlimited) data class Item(val value: T): Event() How Turbine

    Works
  64. Channel (Unlimited) data class Item(val value: T): Event() How Turbine

    Works
  65. Channel (Unlimited) object Complete: Event() How Turbine Works

  66. Channel (Unlimited) Exception data class Error(""...): Event() How Turbine Works

  67. Channel (Unlimited) How do I query items in channels? How

    Turbine Works
  68. interface FlowTurbine<T> { val timeout: Duration fun expectNoEvents() suspend fun

    expectItem(): T fun expectError(): Throwable suspend fun expectComplete() } API to verify emissions How Turbine Works
  69. interface FlowTurbine<T> { val timeout: Duration fun expectNoEvents() suspend fun

    expectItem(): T fun expectError(): Throwable suspend fun expectComplete() } Verify duration of emission How Turbine Works
  70. interface FlowTurbine<T> { val timeout: Duration fun expectNoEvents() suspend fun

    expectItem(): T fun expectError(): Throwable suspend fun expectComplete() } Verify no emissions How Turbine Works
  71. interface FlowTurbine<T> { val timeout: Duration fun expectNoEvents() suspend fun

    expectItem(): T fun expectError(): Throwable suspend fun expectComplete() } Verify item emitted How Turbine Works
  72. interface FlowTurbine<T> { val timeout: Duration fun expectNoEvents() suspend fun

    expectItem(): T fun expectError(): Throwable suspend fun expectComplete() } Verify error occurred How Turbine Works
  73. interface FlowTurbine<T> { val timeout: Duration fun expectNoEvents() suspend fun

    expectItem(): T fun expectError(): Throwable suspend fun expectComplete() } Verify all items emitted How Turbine Works
  74. interface FlowTurbine<T> { val timeout: Duration fun expectNoEvents() suspend fun

    expectItem(): T fun expectError(): Throwable suspend fun expectComplete() } How Turbine Works
  75. How Turbine Works suspend fun <T> Flow<T>.test(""...) { coroutineScope {

    } Create a coroutine
  76. How Turbine Works suspend fun <T> Flow<T>.test(""...) { coroutineScope {

    val events = Channel<Event<T">>(UNLIMITED) } Channel of events
  77. How Turbine Works suspend fun <T> Flow<T>.test(""...) { coroutineScope {

    val events = Channel<Event<T">>(UNLIMITED) launch { } }
  78. How Turbine Works suspend fun <T> Flow<T>.test(""...) { coroutineScope {

    val events = Channel<Event<T">>(UNLIMITED) launch { collect { item "-> events.send(Event.Item(item)) } } Store emissions
  79. How Turbine Works suspend fun <T> Flow<T>.test(""...) { coroutineScope {

    val events = Channel<Event<T">>(UNLIMITED) launch { try { collect { ""... } } catch { events.send(Event.Error) } } Send error to Channel
  80. How Turbine Works suspend fun <T> Flow<T>.test( validate: suspend FlowTurbine<T>.()

    "-> Unit ) { coroutineScope { val events = Channel<Event<T">>(UNLIMITED) launch { try { collect { ""... } } catch { events.send(Event.Error) Lambda block
  81. How Turbine Works suspend fun <T> Flow<T>.test( validate: suspend FlowTurbine<T>.()

    "-> Unit ) { coroutineScope { val events = Channel<Event<T">>(UNLIMITED) launch { try { collect { ""... } } catch { events.send(Event.Error) API
  82. @Test fun `should get users`() = runBlockingTest { val users

    = usersRepository.getUsers().test { expectItem() shouldBe "User 2" expectItem() shouldBe "User 5" expectComplete() } } Read emissions with API
  83. Flow Test Cases • Collection • Run on Dispatcher •

    Errors • Retries Fail • Retry Succeeds
  84. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } Run on Dispatcher
  85. class UsersRepository( val apiService: ApiService, val dispatcher: CoroutineDispatcher ) {

    fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC" } .map { Result.success(it.name) } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Inject Dispatcher
  86. class UsersRepository( val apiService: ApiService, val dispatcher: CoroutineDispatcher ) {

    fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC" } .map { Result.success(it.name) } .flowOn(dispatcher) } Use Dispatcher
  87. class UsersRepository( val apiService: ApiService, val dispatcher: CoroutineDispatcher ) {

    fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC" } .map { Result.success(it.name) } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }
  88. @Test fun `should get users`() = runBlockingTest { usersRepository.getUsers().test {

    expectError() } } How do I use a custom dispatcher?
  89. @Test fun `should get users`() = runBlockingTest { usersRepository.getUsers().test {

    expectError() } } fun TestCoroutineDispatcher.runBlockingTest(""...)
  90. val dispatcher = TestCoroutineDispatcher() val usersRepository = UsersRepository(""..., dispatcher) @Test

    fun `should get users`() = dispatcher.runBlockingTest { usersRepository.getUsers().test { expectError() } }
  91. val dispatcher = TestCoroutineDispatcher() val usersRepository = UsersRepository(""..., dispatcher) @Test

    fun `should get users`() = dispatcher.runBlockingTest { usersRepository.getUsers().test { expectError() } }
  92. val dispatcher = TestCoroutineDispatcher() val usersRepository = UsersRepository(""..., dispatcher) @Test

    fun `should get users`() = dispatcher.runBlockingTest { usersRepository.getUsers().test { expectError() } }
  93. Flow Test Cases • Collection • Run on Dispatcher •

    Errors • Retries Fail • Retry Succeeds
  94. class UsersRepository(val apiService: ApiService) { fun getUsers(): Flow<String> { return

    apiService.getUsers() .filter { it.location "== "NYC" } .map { it.name } } } Test Error?
  95. @Test fun `should handle error`() = dispatcher.runBlockingTest { usersRepository.getUsers().test {

    expectError() } }
  96. @Test fun `should handle error`() = dispatcher.runBlockingTest { whenever(apiService.getUsers()) usersRepository.getUsers().test

    { expectError() } } Mock API method
  97. @Test fun `should handle error`() = dispatcher.runBlockingTest { whenever(apiService.getUsers()) doReturn

    flow { throw Exception() } usersRepository.getUsers().test { expectError() } } Throw exception
  98. @Test fun `should handle error`() = dispatcher.runBlockingTest { whenever(apiService.getUsers()) doReturn

    flow { throw Exception() } usersRepository.getUsers().test { expectError() } } Verify error is thrown
  99. @Test fun `should handle error`() = dispatcher.runBlockingTest { whenever(apiService.getUsers()) doReturn

    flow { throw Exception() } usersRepository.getUsers().test { expectError() } } UserRepoTests.kt Run: should handle error UserRepoTest
  100. @Test fun `should handle error`() = runBlockingTest { whenever(apiService.getUsers()) doReturn

    flow { throw Exception() } usersRepository.getUsers().test { expectError() } } UserRepoTests.kt Run: should handle error UserRepoTest Try Catch
  101. class UsersRepository(val apiService: ApiService) { fun getUsers(): Flow<String> = apiService.getUsers()

    .filter { it.location "== "NYC" } .map { it.name } } Better way to handle errors?
  102. inline class Result { fun <T> success(value: T) fun <T>

    failure(exception: Throwable) } Union to represent success or failure
  103. fun getUsers(): Flow<Result<String">> = return apiService.getUsers() .filter { it.location "==

    "NYC" } .map { Result.success(it.name) } .catch { emit(Result.failure(it)) } Flow emits Results
  104. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .catch { emit(Result.failure(it)) } Success
  105. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .catch { emit(Result.failure(it)) } Emit Result Failure
  106. @Test fun `should handle error`() = dispatcher.runBlockingTest { whenever(apiService.getUsers()) doReturn

    flow { throw Exception() } usersRepository.getUsers().test { } }
  107. @Test fun `should handle error`() = dispatcher.runBlockingTest { whenever(apiService.getUsers()) doReturn

    flow { throw Exception() } usersRepository.getUsers().test { expectItem().isFailure.shouldBeTrue() expectComplete() } } Verify Result is failure
  108. @Test fun `should handle error`() = dispatcher.runBlockingTest { whenever(apiService.getUsers()) doReturn

    flow { throw Exception() } usersRepository.getUsers().test { expectItem().isFailure expectComplete() } } Verify no more emissions
  109. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .catch { emit(Result.failure(it)) } Exception
  110. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .catch { emit(Result.failure(it)) } Emit Result item
  111. Flow Test Cases • Collection • Run on Dispatcher •

    Errors • Retries Fail • Retry Succeeds
  112. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Exception
  113. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Catch Exception
  114. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Retry?
  115. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Retry 2-times
  116. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Delay for each retry
  117. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Error
  118. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Retry
  119. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Delay 2 seconds
  120. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Try running Flow again
  121. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Retry fails, go to catch
  122. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher)
  123. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) How do we test retries?
  124. @Test fun `should retry twice`() = dispatcher.runBlockingTest { usersRepository.getUsers().test {

    advanceTimeBy(2000) advanceTimeBy(2000) expectItem().isFailure.shouldBeTrue() expectComplete() } }
  125. @Test fun `should retry twice`() = dispatcher.runBlockingTest { whenever(apiService.getUsers()) doReturn

    flow { throw IOException() } usersRepository.getUsers().test { advanceTimeBy(2000) advanceTimeBy(2000) expectItem().isFailure.shouldBeTrue() expectComplete() } } Throw Exception
  126. @Test fun `should retry twice`() = dispatcher.runBlockingTest { whenever(apiService.getUsers()) doReturn

    flow { throw IOException() } usersRepository.getUsers().test { expectItem().isFailure.shouldBeTrue() expectComplete() } } Read from Flow
  127. @Test fun `should retry twice`() = dispatcher.runBlockingTest { whenever(apiService.getUsers()) doReturn

    flow { throw IOException() } usersRepository.getUsers().test { expectItem().isFailure.shouldBeTrue() expectComplete() } } Verify Result is Failure
  128. @Test fun `should retry twice`() = dispatcher.runBlockingTest { whenever(apiService.getUsers()) doReturn

    flow { throw IOException() } usersRepository.getUsers().test { expectItem().isFailure.shouldBeTrue() expectComplete() } }
  129. @Test fun `should retry twice`() = dispatcher.runBlockingTest { whenever(apiService.getUsers()) doReturn

    flow { throw IOException() } usersRepository.getUsers().test { expectItem().isFailure.shouldBeTrue() expectComplete() } } UserRepoTests.kt Run: UserRepoTest should retry twice kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms ! ! Timeout Exception
  130. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Error
  131. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Retry
  132. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Delay 2 seconds
  133. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Advance virtual time forward
  134. @Test fun `should retry twice`() = dispatcher.runBlockingTest { whenever(apiService.getUsers()) doReturn

    flow { throw IOException() } usersRepository.getUsers().test { advanceTimeBy(2000) expectItem().isFailure.shouldBeTrue() expectComplete() } } Advance virtual time on dispatcher
  135. @Test fun `should retry twice`() = dispatcher.runBlockingTest { whenever(apiService.getUsers()) doReturn

    flow { throw IOException() } usersRepository.getUsers().test { advanceTimeBy(2000) expectItem().isFailure.shouldBeTrue() expectComplete() } } Advance time by 2 seconds
  136. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Retry
  137. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Delay for before 2nd retry
  138. @Test fun `should retry twice`() = dispatcher.runBlockingTest { whenever(apiService.getUsers()) doReturn

    flow { throw IOException() } usersRepository.getUsers().test { advanceTimeBy(2000) advanceTimeBy(2000) expectItem().isFailure.shouldBeTrue() expectComplete() } } Advance time forward for 2nd retry
  139. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) 2nd Retry
  140. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) All Retries failed
  141. @Test fun `should retry twice`() = dispatcher.runBlockingTest { whenever(apiService.getUsers()) doReturn

    flow { throw IOException() } usersRepository.getUsers().test { advanceTimeBy(2000) advanceTimeBy(2000) expectItem().isFailure.shouldBeTrue() expectComplete() } }
  142. Flow Test Cases • Collection • Run on Dispatcher •

    Errors • Retries Fail • Retry Succeeds
  143. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) 2nd Retry Succeeds
  144. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Apply Filter, Map
  145. var throwError = false whenever(apiService.getUsers()) doReturn flow { if (throwError)

    { throw IOException() } else { emit(User(1, "User 1", "NYC")) } } Flag to control sending error
  146. var throwError = false whenever(apiService.getUsers()) doReturn flow { if (throwError)

    { throw IOException() } else { emit(User(1, "User 1", "NYC")) } } Mock API Service to return Flow
  147. var throwError = false whenever(apiService.getUsers()) doReturn flow { if (throwError)

    { throw IOException() } else { emit(User(1, "User 1", "NYC")) } } Throw Error on exception
  148. var throwError = false whenever(apiService.getUsers()) doReturn flow { if (throwError)

    { throw IOException() } else { emit(User(1, "User 1", "NYC")) } } Emit User
  149. @Test fun `should retry succeed`() = dispatcher.runBlockingTest { usersRepository.getUsers().test {

    advanceTimeBy(2000) throwError = false advanceTimeBy(2000) expectItem().isSuccess.shouldBeTrue() expectComplete() } } Retry
  150. @Test fun `should retry succeed`() = dispatcher.runBlockingTest { usersRepository.getUsers().test {

    advanceTimeBy(2000) throwError = false advanceTimeBy(2000) expectItem().isSuccess.shouldBeTrue() expectComplete() } } Flow will return data
  151. @Test fun `should retry succeed`() = dispatcher.runBlockingTest { usersRepository.getUsers().test {

    advanceTimeBy(2000) throwError = false advanceTimeBy(2000) expectItem().isSuccess.shouldBeTrue() expectComplete() } } 2nd Retry
  152. @Test fun `should retry succeed`() = dispatcher.runBlockingTest { usersRepository.getUsers().test {

    advanceTimeBy(2000) throwError = false advanceTimeBy(2000) expectItem().isSuccess.shouldBeTrue() expectComplete() } } Verify Success
  153. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) 2nd Retry
  154. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher)
  155. fun getUsers(): Flow<Result<String">> = apiService.getUsers() .filter { it.location "== "NYC"

    } .map { Result.success(it.name) } .retry(2) { delay(2000) true } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) Operators
  156. @Test fun `should retry succeed`() = dispatcher.runBlockingTest { usersRepository.getUsers().test {

    advanceTimeBy(2000) throwError = false advanceTimeBy(2000) expectItem().isSuccess.shouldBeTrue() expectComplete() } }
  157. Flow Test Cases • Collection • Run on Dispatcher •

    Errors • Retries Fail • Retry Succeeds
  158. View Model Testing

  159. Flow of users Repository Service

  160. View Model Repository

  161. class UserViewModel(): ViewModel() { val states = MutableStateFlow(State()) fun getData()

    { usersRepository.getUsers() .onEach { states.value = State(data = it.get()) } .launchIn(viewModelScope) } } data class State(val data: String = "") Jetpack View Model
  162. class UserViewModel(val usersRepository: UsersRepository): ViewModel() { val states = MutableStateFlow(State())

    fun getData() { usersRepository.getUsers() .onEach { states.value = State(data = it.get()) } .launchIn(viewModelScope) } } data class State(val data: String = "") Inject Repo
  163. class UserViewModel(val usersRepository: UsersRepository): ViewModel() { val states = MutableStateFlow("")

    fun getData() { usersRepository.getUsers() .onEach { states.value = State(data = it.get()) } .launchIn(viewModelScope) } } State Flow
  164. class UserViewModel(val usersRepository: UsersRepository): ViewModel() { val states = MutableStateFlow("")

    fun getData() { usersRepository.getUsers() .onEach { states.value = State(data = it.get()) } .launchIn(viewModelScope) } } Flow of user names
  165. class UserViewModel(val usersRepository: UsersRepository): ViewModel() { val states = MutableStateFlow("")

    fun getData() { usersRepository.getUsers() .onEach { states.value = it } .launchIn(viewModelScope) } } Update State flow for each emission
  166. class UserViewModel(val usersRepository: UsersRepository): ViewModel() { val states = MutableStateFlow("")

    fun getData() { usersRepository.getUsers() .onEach { states.value = it } .launchIn(viewModelScope) } } Launch coroutine in view module sopce
  167. class UserViewModel(val usersRepository: UsersRepository): ViewModel() { val states = MutableStateFlow("")

    fun getData() { usersRepository.getUsers() .onEach { states.value = it } .launchIn(viewModelScope) } }
  168. val repo = mock<UsersRepository>() val viewModel = UserViewModel(repo) Inject mock

    into view model
  169. @Test fun `should get data`() = runBlockingTest { whenever(repo.getUsers()) doReturn

    flowOf(Result.success("User")) viewModel.getData() viewModel.states.test { expectItem().data shouldBe "User" } }
  170. @Test fun `should get data`() = runBlockingTest { whenever(repo.getUsers()) doReturn

    flowOf(Result.success("User")) viewModel.getData() viewModel.states.test { expectItem().data shouldBe "User" } } Mock flow
  171. @Test fun `should get data`() = runBlockingTest { whenever(repo.getUsers()) doReturn

    flowOf("User") viewModel.getData() viewModel.states.test { expectItem().data shouldBe "User" } } Emit Successful response
  172. @Test fun `should get data`() = runBlockingTest { whenever(repo.getUsers()) doReturn

    flowOf("User") viewModel.getData() viewModel.states.test { expectItem().data shouldBe "User" } } Run test
  173. @Test fun `should get data`() = runBlockingTest { whenever(repo.getUsers()) doReturn

    flowOf("User") viewModel.getData() viewModel.states.test { expectItem().data shouldBe "User" } } Verify emission
  174. @Test fun `should get data`() = runBlockingTest { whenever(repo.getUsers()) doReturn

    flowOf(Result.success("User")) viewModel.getData() viewModel.states.test { expectItem().data shouldBe "User" } } ViewModelTest.kt Run: ViewModelTest should get data Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
  175. val dispatcher = TestCoroutineDispatcher() @Before fun setUp() { Dispatchers.setMain(dispatcher) }

    @After fun tearDown() { Dispatchers.resetMain() } Set Main Dispatcher
  176. val dispatcher = TestCoroutineDispatcher() @Before fun setUp() { Dispatchers.setMain(dispatcher) }

    @After fun tearDown() { Dispatchers.resetMain() } Reset Main Dispatcher
  177. class CoroutineTestRule(testDispatcher) : TestWatcher() { override fun starting(description) { Dispatchers.setMain(testDispatcher)

    } override fun finished(description) { Dispatchers.resetMain() testDispatcher.cleanupTestCoroutines() } }
  178. class CoroutineTestRule(testDispatcher) : TestWatcher() { override fun starting(description) { Dispatchers.setMain(testDispatcher)

    } override fun finished(description) { Dispatchers.resetMain() testDispatcher.cleanupTestCoroutines() } }
  179. class CoroutineTestRule(testDispatcher) : TestWatcher() { override fun starting(description) { Dispatchers.setMain(testDispatcher)

    } override fun finished(description) { Dispatchers.resetMain() testDispatcher.cleanupTestCoroutines() } }
  180. @get:Rule var testRule = CoroutineTestRule() @Test fun `should get data`()

    = runBlockingTest { whenever(repo.getUsers()) doReturn flowOf("User") viewModel.getData() viewModel.states.test { expectItem().data shouldBe "User" } }
  181. @get:Rule var testRule = CoroutineTestRule() @Test fun `should get data`()

    = runBlockingTest { whenever(repo.getUsers()) doReturn flowOf(Result.success("User")) viewModel.getData() viewModel.states.test { expectItem().data shouldBe "User" } } ViewModelTest.kt Run: ViewModelTest should get data
  182. @Test fun `should get data`() = runBlockingTest { whenever(repo.getUsers()) doReturn

    flowOf( "User 1", "User 2" ) viewModel.getData() viewModel.states.test { expectItem().data shouldBe "User 1" expectItem().data shouldBe "User 2" expectComplete() } Return 2 results
  183. @Test fun `should get data`() = runBlockingTest { whenever(repo.getUsers()) doReturn

    flowOf( "User 1", "User 2" ) viewModel.getData() viewModel.states.test { expectItem().data shouldBe "User 1" expectItem().data shouldBe "User 2" expectComplete() } Return 2 results
  184. @Test fun `should get data`() = runBlockingTest { viewModel.getData() viewModel.states.test

    { expectItem().data shouldBe "User 1" expectItem().data shouldBe "User 2" expectComplete() } Verification
  185. @Test fun `should get data`() = runBlockingTest { viewModel.getData() viewModel.states.test

    { expectItem().data shouldBe "User 1" expectItem().data shouldBe "User 2" expectComplete() } ViewModelTest.kt Run: ViewModelTest should get data java.lang.AssertionError: Expected <User 1>, actual <User 2> are not the same instance.
  186. @Test fun `should get data`() = runBlockingTest { viewModel.getData() viewModel.states.test

    { expectItem().data shouldBe "User 1" expectItem().data shouldBe "User 2" expectComplete() } ViewModelTest.kt Run: ViewModelTest should get data java.lang.AssertionError: Expected <User 1>, actual <User 2> are not the same instance. Assertion failed
  187. class UserViewModel(val usersRepository: UsersRepository): ViewModel() { val states = MutableStateFlow(State())

    fun getData() { usersRepository.getUsers() .onEach { states.value = State(it) } .launchIn(viewModelScope) } } Conflated
  188. Channels • Broadcast Channel • Unlimited Channel • Buffered Channel

    • Mutable Shared Flow
  189. class UserViewModel(val usersRepository: UsersRepository): ViewModel() { val sharedFlow = MutableSharedFlow<String>()

    }
  190. class UserViewModel(val usersRepository: UsersRepository): ViewModel() { val sharedFlow = MutableSharedFlow<String>()

    fun getData() { usersRepository.getUsers() .onEach { states.emit(it) } .launchIn(viewModelScope) } }
  191. Channels • Broadcast Channel • Unlimited Channel • Buffered Channel

    • Mutable Shared Flow
  192. Coroutine Lib Testing How tests are written in the coroutines

    lib?
  193. class JobBasicCancellationTest : TestBase() { @Test fun testCancelJobImpl() { }

    } Defines utilities
  194. class JobBasicCancellationTest : TestBase() { @Test fun testCancelJobImpl() { }

    } Test parent job is not cancelled
  195. @Test fun testCancelJobImpl() = runTest { val parent = launch

    { expect(1) val child = Job(coroutineContext[Job]) expect(2) child.cancel() child.join() expect(3) } parent.join() finish(4) }
  196. @Test fun testCancelJobImpl() = runTest { val parent = launch

    { expect(1) val child = Job(coroutineContext[Job]) expect(2) child.cancel() child.join() expect(3) } parent.join() finish(4) } Launch coroutine
  197. @Test fun testCancelJobImpl() = runTest { val parent = launch

    { expect(1) val child = Job(coroutineContext[Job]) expect(2) child.cancel() child.join() expect(3) } parent.join() finish(4) } Child coroutine Job
  198. @Test fun testCancelJobImpl() = runTest { val parent = launch

    { expect(1) val child = Job(coroutineContext[Job]) expect(2) child.cancel() child.join() expect(3) } parent.join() finish(4) } Cancel the job
  199. @Test fun testCancelJobImpl() = runTest { val parent = launch

    { expect(1) val child = Job(coroutineContext[Job]) expect(2) child.cancel() child.join() expect(3) } parent.join() finish(4) } Verify order of execution Verify completion
  200. @Test fun testCancelJobImpl() = runTest { val parent = launch

    { expect(1) val child = Job(coroutineContext[Job]) expect(2) child.cancel() child.join() expect(3) } parent.join() finish(4) }
  201. expect open class TestBase { fun error(message, cause) fun expect(index:

    Int) fun finish(index: Int) fun runTest( block: suspend CoroutineScope.() "-> Unit ) }
  202. JVM JS Native Common TestBase.kt TestBase.kt TestBase.kt TestBase.kt

  203. actual fun runTest( block: suspend CoroutineScope.() "-> Unit ) {

    runBlocking( block = block, context = CoroutineExceptionHandler { ""... }) }
  204. expect open class TestBase { fun error(message, cause) fun expect(index:

    Int) fun finish(index: Int) fun runTest( block: suspend CoroutineScope.() "-> Unit ) } How does this work?
  205. private var actionIndex = AtomicInteger() actual fun expect(index: Int) {

    val wasIndex = actionIndex.incrementAndGet() check(index "== wasIndex) { “Expecting action index $index but it is actually $wasIndex" } }
  206. private var actionIndex = AtomicInteger() actual fun expect(index: Int) {

    val wasIndex = actionIndex.incrementAndGet() check(index "== wasIndex) { “Expecting action index $index but it is actually $wasIndex" } }
  207. private var actionIndex = AtomicInteger() actual fun expect(index: Int) {

    val wasIndex = actionIndex.incrementAndGet() check(index "== wasIndex) { “Expecting action index $index but it is actually $wasIndex" } }
  208. expect open class TestBase { fun error(message, cause) fun expect(index:

    Int) fun finish(index: Int) fun runTest( block: suspend CoroutineScope.() "-> Unit ) } How does this work?
  209. private var finished = AtomicBoolean() actual fun finish(index: Int) {

    expect(index) check(!finished.getAndSet(true)) { "Should call 'finish(""...)' at most once" } }
  210. private var finished = AtomicBoolean() actual fun finish(index: Int) {

    expect(index) check(!finished.getAndSet(true)) { "Should call 'finish(""...)' at most once" } }
  211. private var finished = AtomicBoolean() actual fun finish(index: Int) {

    expect(index) check(!finished.getAndSet(true)) { "Should call 'finish(""...)' at most once" } }
  212. @Test fun testCancelJobImpl() = runTest { val parent = launch

    { expect(1) val child = Job(coroutineContext[Job]) expect(2) child.cancel() child.join() expect(3) } parent.join() finish(4) }
  213. https:"//codingwithmohit.com/coroutines/unit-testing-delays-errors-retries-with-kotlin-flows/ Unit Testing

  214. Resources • Unit Testing Delays, Errors & Retries with Kotlin

    Flows https:"//codingwithmohit.com/coroutines/unit-testing-delays- errors-retries-with-kotlin-flows/ • Unit Testing Channels & Flows in Practice https:"//www.youtube.com/watch?v=h8bLIUi6HWU
  215. Thank You! www.twitter.com/heyitsmohit www.codingwithmohit.com