Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Unit Testing Channels & Flows

Unit Testing Channels & Flows

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.

https://youtu.be/h8bLIUi6HWU

Mohit S

June 03, 2020
Tweet

More Decks by Mohit S

Other Decks in Programming

Transcript

  1. Unit Test Kotlin Channels & Flows • Use case with

    ViewModel & Repo • Testing Repo & ViewModel • Flow Assertions • Test Pattern used in Coroutine Library
  2. fun userDetails(id: Int): Flow<Result<UserDetails">> { } fun <T> flow( block:

    suspend FlowCollector<T>.() "-> Unit ): Flow<T> Creating Flow Coroutine Library
  3. fun userDetails(id: Int): Flow<Result<UserDetails">> { } interface FlowCollector<in T> {

    suspend fun emit(value: T) } Coroutine Library Creating Flow
  4. Creating Flow fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow {

    val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) } }
  5. Creating Flow fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow {

    val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) } } separate thread?
  6. Creating Flow fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow {

    val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) } } Coroutine Library fun Flow<T>.flowOn(context: CoroutineContext): Flow<T>
  7. Creating Flow fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow {

    val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) } } Coroutine Library Dispatcher • Default, IO, Main, etc""...
  8. Creating Flow class UserRepository( val apiService: ApiService, val dispatcher: CoroutineDispatcher

    ) { fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val users = apiService.userDetails(id) emit(Result.success(users)) }.flowOn(dispatcher) }
  9. Creating Flow class UserRepository( val apiService: ApiService, val dispatcher: CoroutineDispatcher

    ) { fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val users = apiService.userDetails(id) emit(Result.success(users)) }.flowOn(dispatcher) }
  10. View Model class UserDetailViewModel : ViewModel { val scope =

    CoroutineScope(Dispatchers.Main) } Coroutine Library fun CoroutineScope(context: CoroutineContext)
  11. View Model class UserDetailViewModel : ViewModel { val scope =

    CoroutineScope(Dispatchers.Main) scope.launch { } }
  12. View Model class UserDetailViewModel( val repository: UserRepository ) : ViewModel

    { val scope = CoroutineScope(Dispatchers.Main) scope.launch { } }
  13. View Model class UserDetailViewModel( val repository: UserRepository ) : ViewModel

    { val scope = CoroutineScope(Dispatchers.Main) scope.launch { val flow = repository.getUserDetails(id = 1) } }
  14. View Model class UserDetailViewModel( val repository: UserRepository ) : ViewModel

    { val scope = CoroutineScope(Dispatchers.Main) scope.launch { val flow = repository.getUserDetails(id = 1) } } Coroutine Library fun <T> Flow<T>.collect( action: suspend (value: T) "-> Unit )
  15. View Model class UserDetailViewModel( val repository: UserRepository ) : ViewModel

    { val scope = CoroutineScope(Dispatchers.Main) scope.launch { val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> "-> } }
  16. View Model class UserDetailViewModel( val repository: UserRepository val reducer: Reducer

    ) : ViewModel { val scope = CoroutineScope(Dispatchers.Main) scope.launch { val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> "-> reducer.dispatchState(result) } }
  17. View Model class UserDetailViewModel( val repository: UserRepository, val reducer: Reducer

    ) : ViewModel { val scope = CoroutineScope(Dispatchers.Main) scope.launch { val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> "-> reducer.dispatchState(result) } }
  18. val ViewModel.viewModelScope: CoroutineScope get() { val scope: CoroutineScope? = this.getTag(JOB_KEY)

    return scope } View Model class UserDetailViewModel( val repository: UserRepository, val stateManager: StateManager ) : ViewModel { val scope = CoroutineScope(Dispatchers.Main) Architecture Components
  19. Kotlin Coroutines Test Library • Test Coroutine Scope • Test

    Coroutine Dispatcher • runBlockingTest testImplementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-test:x.x.x'
  20. Unit Test Repository Cases • Flow emits Success • Flow

    emits Error • Retries with delay (Advanced)
  21. Repository class UserRepository( val apiService: ApiService, val dispatcher: CoroutineDispatcher )

    { fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val users = apiService.userDetails(id) emit(Result.success(users)) }.flowOn(dispatcher) }
  22. @Test fun `should get users details on success`() = runBlocking

    { } Coroutine Library RunBlocking • Creates a coroutine • Blocks until coroutine completes
  23. @Test fun `should get users details on success`() = runBlocking

    { val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock<ApiService>() } suspend fun userDetails(id: Int): UserDetails API Service
  24. @Test fun `should get users details on success`() = runBlocking

    { val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock<ApiService>() { on { userDetails(1) } doReturn userDetails } Error: Suspend functions can only be called from suspend functions
  25. @Test fun `should get users details on success`() = runBlocking

    { val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock<ApiService>() { on { userDetails(1) } doReturn userDetails } } Mockito Kotlin fun <R> on(methodCall: T.() "-> R)
  26. Mockito Kotlin fun onBlocking(m: suspend T.() "-> R) { return

    runBlocking { Mockito.`when`(mock.m()) } } @Test fun `should get users details on success`() = runBlocking { val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock<ApiService>() { on { userDetails(1) } doReturn userDetails } }
  27. @Test fun `should get users details on success`() = runBlocking

    { val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock<ApiService>() { onBlocking { userDetails(1) } doReturn userDetails } }
  28. @Test fun `should get users details on success`() = runBlocking

    { val userDetails = mockUserDetails() }
  29. @Test fun `should get users details on success`() = runBlocking

    { val userDetails = mockUserDetails() val dispatcher = TestCoroutineDispatcher() val repository = UserRepository(userService, dispatcher) val flow = repository.getUserDetails(id = 1) }
  30. @Test fun `should get users details on success`() = runBlocking

    { val userDetails = mockUserDetails() val dispatcher = TestCoroutineDispatcher() val repository = UserRepository(userService, dispatcher) val flow = repository.getUserDetails(id = 1) val result = flow.single() result.isSuccess.assertTrue() }
  31. Unit Test Repository Cases • Flow emits Success • Flow

    emits Error • Retries with delay (Advanced)
  32. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.flowOn(dispatcher) } Exception
  33. View Model scope.launch { val flow: = userRepository.getUserDetails(id = 1)

    flow.collect { } } fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails = apiService.userDetails(id) emit(Result.success(userDetails)) }.flowOn(dispatcher) } Exception
  34. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.flowOn(dispatcher) } Coroutine Library fun <T> Flow<T>.catch( action: suspend FlowCollector<T>.(t: Throwable) "-> Unit ): Flow<T>
  35. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Exception
  36. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Exception
  37. @Test fun `should get error for user details`() = runBlocking

    { val apiService = mock<ApiService> { onBlocking { userDetails(1) } doAnswer { throw IOException() } } } Mock
  38. @Test fun `should get error for user details`() = runBlocking

    { val apiService = mockApiService() val repository = UserRepository(apiService, dispatcher) val flow = repository.getUserDetails(id = 1) }
  39. @Test fun `should get error for user details`() = runBlocking

    { val apiService = mockApiService() val repository = UserRepository(apiService, dispatcher) val flow = repository.getUserDetails(id = 1) val result = flow.single() result.isFailure.assertTrue() }
  40. Unit Test Repository Cases • Flow emits Success • Flow

    emits Error • Retries with delay (Advanced)
  41. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Exception
  42. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Coroutine Library fun retry( retries: Long, block: suspend (Throwable) "-> Boolean ): Flow<T>
  43. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t: Throwable "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }
  44. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.catch { emit(Result.failure(it)) } .retry(retries = 2) { t: Throwable "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Exception
  45. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.catch { emit(Result.failure(it)) } .retry(retries = 2) { t: Throwable "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Retry
  46. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.catch { emit(Result.failure(it)) } .retry(retries = 2) { t: Throwable "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Delay 1s
  47. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.catch { emit(Result.failure(it)) } .retry(retries = 2) { t: Throwable "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Retry
  48. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) }
  49. runBlockingTest 1. Launch a coroutine with a test scope and

    test dispatcher. 2. Advance virtual time forward.
  50. @Test fun `should retry with error`() { } Coroutine Library

    fun runBlockingTest( block: suspend TestCoroutineScope.() "-> Unit ) fun TestCoroutineDispatcher.runBlockingTest( block: suspend TestCoroutineScope.() "-> Unit )
  51. val dispatcher = TestCoroutineDispatcher() @Test fun `should retry with error`()

    = dispatcher.runBlockingTest { } Create coroutines with test dispatcher
  52. @Test fun `should retry with error`() = dispatcher.runBlockingTest { val

    apiService = mock<ApiService> { onBlocking { userDetails(1) } doAnswer { throw IOException() } } }
  53. @Test fun `should retry with error`() = dispatcher.runBlockingTest { val

    apiService = mockApiService() val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> "-> result.isFailure.assertTrue() } }
  54. @Test fun `should retry with error`() = dispatcher.runBlockingTest { val

    apiService = mockApiService() val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> "-> result.isFailure.assertTrue() } } runBlockingTest
  55. @Test fun `should retry with error`() = dispatcher.runBlockingTest { val

    apiService = mockApiService() val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> "-> result.isFailure.assertTrue() } } runBlockingTest
  56. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } runBlockingTest
  57. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } runBlockingTest
  58. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val users

    = userService.userDetails(id) emit(Result.success(users)) }.retry(retries = 2) { t "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } runBlockingTest Coroutines Test Library fun runBlockingTest { dispatcher.advanceUntilIdle() }
  59. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) runBlockingTest
  60. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t: Throwable "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Catch Error. All retries failed.
  61. @Test fun `should retry with error`() = dispatcher.runBlockingTest { val

    apiService = mockApiService() val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> "-> result.isFailure.assertTrue() } }
  62. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Retry
  63. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Success
  64. @Test fun `should retry with success`() = dispatcher.runBlockingTest { var

    throwError = true val userDetails = UserDetails(1, "User 1", "avatar_url") } Successful Response
  65. @Test fun `should retry with success`() = dispatcher.runBlockingTest { var

    throwError = true val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock<ApiService> { } }
  66. @Test fun `should retry with success`() = dispatcher.runBlockingTest { var

    throwError = true val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock<ApiService> { onBlocking { userDetails(1) } doAnswer { if (throwError) throw IOException() else userDetails } } } Control API Response
  67. @Test fun `should retry with success`() = dispatcher.runBlockingTest { val

    apiService = mockApiService() pauseDispatcher { } } Coroutine Test Library pausedDispatcher • Do not start coroutines eagerly
  68. @Test fun `should retry with success`() = dispatcher.runBlockingTest { val

    apiService = mockApiService() pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } } Consumer
  69. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t: Throwable "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Run Flow
  70. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t: Throwable "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Suspend
  71. @Test fun `should retry with success`() = dispatcher.runBlockingTest { val

    apiService = mockApiService() pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } advanceTimeBy(DELAY_ONE_SECOND) } } Advance virtual time
  72. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t: Throwable "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Retry
  73. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t: Throwable "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Suspend
  74. @Test fun `should retry with success`() = dispatcher.runBlockingTest { val

    apiService = mockApiService() pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } advanceTimeBy(DELAY_ONE_SECOND) throwError = false } } Return success
  75. @Test fun `should retry with success`() = dispatcher.runBlockingTest { val

    apiService = mockApiService() pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } advanceTimeBy(DELAY_ONE_SECOND) throwError = false advanceTimeBy(DELAY_ONE_SECOND) } } Advance virtual time
  76. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t: Throwable "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Retry
  77. fun userDetails(id: Int): Flow<Result<UserDetails">> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.retry(retries = 2) { t: Throwable "-> (t is Exception).also { if (it) delay(DELAY_ONE_SECOND) } } .catch { emit(Result.failure(it)) } .flowOn(dispatcher) } Emit Success
  78. @Test fun `should retry with success`() = dispatcher.runBlockingTest { val

    apiService = mockApiService() pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } advanceTimeBy(DELAY_ONE_SECOND) throwError = false advanceTimeBy(DELAY_ONE_SECOND) } } Collect
  79. @Test fun `should retry with success`() = dispatcher.runBlockingTest { val

    apiService = mockApiService() pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } advanceTimeBy(DELAY_ONE_SECOND) throwError = false advanceTimeBy(DELAY_ONE_SECOND) } }
  80. View Model class UserDetailViewModel( val repository: UserRepository, val stateManager: StateManager

    ) : ViewModel { viewModelScope.launch { val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> "-> stateManager.dispatchState(result) } } }
  81. View Model class UserDetailViewModel( val repository: UserRepository, val stateManager: StateManager

    ) : ViewModel { viewModelScope.launch { val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> "-> stateManager.dispatchState(result) } } } Main Dispatcher
  82. Test Rule class CoroutineTestRule( val dispatcher= TestCoroutineDispatcher() ) : TestWatcher()

    { override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(dispatcher) } }
  83. Test Rule class CoroutineTestRule( val dispatcher= TestCoroutineDispatcher() ) : TestWatcher()

    { override fun finished(description: Description?) { super.finished(description) Dispatchers.resetMain() dispatcher.cleanupTestCoroutines() } }
  84. View Model Test @get:Rule val rule = CoroutineTestRule() val repository

    = mock<UserRepository>() val stateManager = mock<StateManager>() val viewModel = UserDetailsViewModel(repository, stateManager)
  85. class UserDetailViewModel( val repository: UserRepository, val stateManager: StateManager ) :

    ViewModel { viewModelScope.launch { val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> "-> stateManager.dispatchState(result) } } } Mock Repo
  86. class UserDetailViewModel( val repository: UserRepository, val stateManager: StateManager ) :

    ViewModel { viewModelScope.launch { val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> "-> stateManager.dispatchState(result) } } } Trigger Flow Collection
  87. @Test fun `should dispatch details`() = rule.dispatcher.runBlockingTest { val userDetails

    = UserDetails(1, "User 1", "avatar") val result = Result.success(userDetails) }
  88. @Test fun `should dispatch details`() = rule.dispatcher.runBlockingTest { val userDetails

    = UserDetails(1, "User 1", "avatar") val result = Result.success(userDetails) val channel = Channel<Result<UserDetails">>() } Channel
  89. @Test fun `should dispatch details`() = rule.dispatcher.runBlockingTest { val userDetails

    = UserDetails(1, "User 1", "avatar") val result = Result.success(userDetails) val channel = Channel<Result<UserDetails">>() val flow = channel.consumeAsFlow() } Convert to Flow Flow Channel Convert to Flow
  90. @Test fun `should dispatch details`() = rule.dispatcher.runBlockingTest { val userDetails

    = UserDetails(1, "User 1", "avatar") val result = Result.success(userDetails) val channel = Channel<Result<UserDetails">>() val flow = channel.consumeAsFlow() whenever(repository.getUserDetails(id = 1)) doReturn flow } Return Flow from Channel
  91. @Test fun `should dispatch details`() = rule.dispatcher.runBlockingTest { val result

    = mockUserDetailsResult() val channel = Channel<Result<UserDetails">>() val flow = channel.consumeAsFlow() whenever(repository.getUserDetails(id = 1)) doReturn flow launch { channel.send(result) } } Producer to send values
  92. @Test fun `should dispatch details`() = rule.dispatcher.runBlockingTest { val result

    = mockUserDetailsResult() val channel = Channel<Result<UserDetails">>() val flow = channel.consumeAsFlow() whenever(repository.getUserDetails(id = 1)) doReturn flow launch { channel.send(result) } userDetailsViewModel.getUserDetails() } Run test method
  93. class UserDetailViewModel( val repository: UserRepository, val stateManager: StateManager ) :

    ViewModel { viewModelScope.launch { val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> "-> stateManager.dispatchState(result) } } } Collect result from Flow
  94. @Test fun `should dispatch details`() = rule.dispatcher.runBlockingTest { val result

    = mockUserDetailsResult() val channel = Channel<Result<UserDetails">>() val flow = channel.consumeAsFlow() whenever(repository.getUserDetails(id = 1)) doReturn flow launch { channel.send(result) } userDetailsViewModel.getUserDetails() verify(stateManager).dispatch(result) } Verify result
  95. Flow Assertions @Test fun `should get users details on success`()

    = runBlocking { ""... flow.collect { } flow.single() } RxJava Observable.test()
  96. sealed class Event<out T> { object Complete : Event<Nothing>() data

    class Error(val t: Throwable) : Event<Nothing>() data class Item<T>(val item: T) : Event<T>() } Flow Assertions Test Flow Channel
  97. Flow Assertions sealed class Event<out T> { object Complete :

    Event<Nothing>() data class Error(val t: Throwable) : Event<Nothing>() data class Item<T>(val item: T) : Event<T>() } Item Test Flow Channel
  98. Flow Assertions sealed class Event<out T> { object Complete :

    Event<Nothing>() data class Error(val t: Throwable) : Event<Nothing>() data class Item<T>(val item: T) : Event<T>() } Test Flow Channel Flow emission complete
  99. sealed class Event<out T> { object Complete : Event<Nothing>() data

    class Error(val t: Throwable) : Event<Nothing>() data class Item<T>(val item: T) : Event<T>() } Flow Assertions Test Flow Channel Error
  100. Flow Assertions suspend fun <T> Flow<T>.test( validate: suspend FlowAssert<T>.() "->

    Unit ) { coroutineScope { val events = Channel<Event<T">>(UNLIMITED) } } Unlimited Buffered Channel
  101. Flow Assertions suspend fun <T> Flow<T>.test( validate: suspend FlowAssert<T>.() "->

    Unit ) { coroutineScope { val events = Channel<Event<T">>(UNLIMITED) launch { collect { item "-> events.send(Event.Item(item)) } Event.Complete } } } Send to Channel
  102. Flow Assertions suspend fun <T> Flow<T>.test( validate: suspend FlowAssert<T>.() "->

    Unit ) { coroutineScope { val events = Channel<Event<T">>(UNLIMITED) launch { try { sendToChannel() } catch (t: Throwable) { send(Event.Error(t)) } } } } Send Error
  103. Flow Assertions suspend fun <T> Flow<T>.test( validate: suspend FlowAssert<T>.() "->

    Unit ) { coroutineScope { val events = Channel<Event<T">>(UNLIMITED) launch { collect { item "-> events.send(Event.Item(item)) } Event.Complete } } }
  104. Flow Assertions class FlowAssert<T>(val events: Channel<Event<T">>) { suspend fun expectItem():

    T suspend fun expectComplete() suspend fun expectError(): Throwable … }
  105. Flow Assertions @Test fun `should get users details on success`()

    = runBlocking { flow.test { expectItem() assertEquals userDetails expectComplete() } }
  106. Kotlin Coroutines Library Testing class CoroutinesTest : TestBase() { @Test

    fun testSimple() = runTest { expect(1) finish(2) } } Expect
  107. Kotlin Coroutines Library Testing class CoroutinesTest : TestBase() { @Test

    fun testSimple() = runTest { expect(1) finish(2) } } Finish
  108. Kotlin Coroutines Library Testing class CoroutinesTest : TestBase() { @Test

    fun testSimple() = runTest { expect(1) finish(2) } } Test Base
  109. Kotlin Coroutines Library Testing expect open class TestBase { fun

    error(message: Any, cause: Throwable? = null): Nothing fun expect(index: Int) fun finish(index: Int) fun runTest( expected: ((Throwable) "-> Boolean)? = null, unhandled: List<(Throwable) "-> Boolean> = emptyList(), block: suspend CoroutineScope.() "-> Unit )
  110. Kotlin Coroutines Library Testing actual open class TestBase actual constructor()

    { actual fun runTest( expected: ((Throwable) "-> Boolean)? = null, unhandled: List<(Throwable) "-> Boolean> = emptyList(), block: suspend CoroutineScope.() "-> Unit ) { runBlocking( block = block, context = CoroutineExceptionHandler { }) }
  111. Kotlin Coroutines Library Testing actual open class TestBase actual constructor()

    { actual fun runTest( expected: ((Throwable) "-> Boolean)? = null, unhandled: List<(Throwable) "-> Boolean> = emptyList(), block: suspend CoroutineScope.() "-> Unit ) { runBlocking( block = block, context = CoroutineExceptionHandler { }) }
  112. Kotlin Coroutines Library Testing class CoroutinesTest : TestBase() { @Test

    fun testSimple() = runTest { expect(1) finish(2) } } Expect
  113. Kotlin Coroutines Library Testing private var actionIndex = AtomicInteger() actual

    fun expect(index: Int) { val wasIndex = actionIndex.incrementAndGet() if (VERBOSE) println("expect($index), wasIndex=$wasIndex") check(index "== wasIndex) { “Expecting action index $index but it is actually $wasIndex" } }
  114. Kotlin Coroutines Library Testing class CoroutinesTest : TestBase() { @Test

    fun testSimple() = runTest { expect(1) finish(2) } } Finish
  115. Kotlin Coroutines Library Testing private var finished = AtomicBoolean() actual

    fun finish(index: Int) { expect(index) check(!finished.getAndSet(true)) { "Should call 'finish(""...)' at most once" } }
  116. Kotlin Coroutines Library Testing class CoroutinesTest : TestBase() { @Test

    fun testSimple() = runTest { expect(1) finish(2) } }
  117. Resources • Unit Testing Delays, Errors & Retries with Kotlin

    Flows https:"//codingwithmohit.com/coroutines/unit-testing-delays- errors-retries-with-kotlin-flows/ • Kotlin Assert Flow Delight https:"//codingwithmohit.com/coroutines/kotlin-assert-flow-delight/ • Channels & Flows in Practice https:"//speakerdeck.com/heyitsmohit/channels-and-flows-in-practice