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

Unit Testing Kotlin Channels & Flows

Mohit S
February 11, 2020

Unit Testing Kotlin Channels & Flows

Mohit will share with you best practices for testing Channels and Flows. He will show practical use cases where we have to handle retries, errors and delays. For each of these scenarios, he will show you patterns to use for testing. We will look at features of the coroutine testing library such as advancing time forward. We will also look at ways to do assertions on a Flow.

Mohit S

February 11, 2020
Tweet

More Decks by Mohit S

Other Decks in Programming

Transcript

  1. • Use case with ViewModel & Repo • Testing Repo

    & ViewModel • Best practices for unit testing
  2. fun userDetails(id: Int): Flow<Result<UserDetails.> { } Coroutines Library fun <T>

    flow( block: suspend FlowCollector<T>.() .> Unit ): Flow<T>
  3. fun userDetails(id: Int): Flow<Result<UserDetails.> { return flow { val userDetails

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

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

    = apiService.userDetails(id) emit(Result.success(userDetails)) } } Coroutines Library Dispatcher - Control scheduling threads - Default, IO, Main, etc...
  6. 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) }
  7. 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) }
  8. class UserDetailViewModel : ViewModel { val scope = CoroutineScope(Dispatchers.Main) }

    Coroutines Library fun CoroutineScope(context: CoroutineContext)
  9. class UserDetailViewModel(val repository: UserRepository) : ViewModel { val scope =

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

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

    CoroutineScope(Dispatchers.Main) scope.launch { val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> -> } } }
  12. class UserDetailViewModel( val repository: UserRepository, val stateManager: StateManager ) :

    ViewModel { val scope = CoroutineScope(Dispatchers.Main) scope.launch { val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> -> stateManager.dispatchState(result) } } }
  13. class UserDetailViewModel( val repository: UserRepository, val stateManager: StateManager ) :

    ViewModel { val scope = CoroutineScope(Dispatchers.Main) scope.launch { val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> -> stateManager.dispatchState(result) } } }
  14. class UserDetailViewModel( val repository: UserRepository, val stateManager: StateManager ): ViewModel

    { { val scope = CoroutineScope(Dispatchers.Main) } Architecture Components val ViewModel.viewModelScope: CoroutineScope get() { val scope: CoroutineScope? = this.getTag(JOB_KEY) return scope }
  15. class UserRepository( val apiService: ApiService, val dispatcher: CoroutineDispatcher ) {

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

    { } Coroutines Library RunBlocking - Blocks untils coroutine completes
  17. @Test fun `should get users details on success`() = runBlocking

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

    { val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock<ApiService>() } API Service suspend fun userDetails(id: Int): UserDetails
  19. @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 } }
  20. @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)
  21. @Test fun `should get users details on success`() = runBlocking

    { val userDetails = UserDetails(1, "User 1", "avatar_url") val apiService = mock<ApiService>() } Mockito-Kotlin fun onBlocking(m: suspend T.() .> R) { return runBlocking { Mockito.`when`(mock.m()) } }
  22. @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 } }
  23. @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 } val dispatcher = TestCoroutineDispatcher() val repository = UserRepository(userService, dispatcher) val flow = repository.getUserDetails(id = 1) }
  24. @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 } val dispatcher = TestCoroutineDispatcher() val repository = UserRepository(userService, dispatcher) val flow = repository.getUserDetails(id = 1) val result = flow.single() result: Result<UserDetails> result.isSuccess.assertTrue() }
  25. fun userDetails(id: Int): Flow<Result<UserDetails.> { return flow { val userDetails

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

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.flowOn(dispatcher) } ViewModel scope.launch { val flow: = userRepository.getUserDetails(id = 1) flow.collect { } }
  27. fun userDetails(id: Int): Flow<Result<UserDetails.> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.flowOn(dispatcher) } Coroutines-Library fun <T> Flow<T>.catch( action: suspend FlowCollector<T>.(t: Throwable) .> Unit ): Flow<T>
  28. 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) }
  29. @Test fun `should get error for user details`() = runBlocking

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

    { val apiService = mock<ApiService> { onBlocking { userDetails(1) } doAnswer { throw IOException() } } val repository = UserRepository(apiService, dispatcher) val flow = repository.getUserDetails(id = 1) val result = flow.single() result.isFailure.assertTrue() }
  31. fun userDetails(id: Int): Flow<Result<UserDetails.> { return flow { val userDetails

    = apiService.userDetails(id) emit(Result.success(userDetails)) }.catch { emit(Result.failure(it)) } } Coroutines-Library fun retry( retries: Long, block: suspend (Throwable) .>Boolean ): Flow<T>
  32. 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) }
  33. 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) }
  34. 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) }
  35. fun userDetails(id: Int): Flow<Result<UserDetails.> { return flow { val userDetails

    = apiService.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) }
  36. 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) }
  37. 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) }
  38. 1. Launch a coroutine with a test scope and test

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

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

    apiService = mock<ApiService> { onBlocking { userDetails(1) } doAnswer { throw IOException() } } val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> -> result.isFailure.assertTrue() } }
  41. @Test fun `should retry with error`() = dispatcher.runBlockingTest { val

    apiService = mock<ApiService> { onBlocking { userDetails(1) } doAnswer { throw IOException() } } val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> -> result.isFailure.assertTrue() } }
  42. @Test fun `should retry with error`() = dispatcher.runBlockingTest { val

    apiService = mock<ApiService> { onBlocking { userDetails(1) } doAnswer { throw IOException() } } val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> -> result.isFailure.assertTrue() } }
  43. 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) }
  44. 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) }
  45. 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) }
  46. 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) } Coroutine-Test-Library fun runBlockingTest { dispatcher.advanceUntilIdle() }
  47. 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) }
  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. @Test fun `should retry with error`() = dispatcher.runBlockingTest { val

    apiService = mock<ApiService> { onBlocking { userDetails(1) } doAnswer { throw IOException() } } val flow = repository.getUserDetails(id = 1) flow.collect { result: Result<UserDetails> -> result.isFailure.assertTrue() } }
  50. 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) }
  51. @Test fun `should retry with success`() = dispatcher.runBlockingTest { pauseDispatcher

    { } } Coroutine-Test-Library pausedDispatcher - Do not start coroutines eagerly
  52. @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 } } }
  53. @Test fun `should retry with success`() = dispatcher.runBlockingTest { var

    throwError = true val apiService = mock<ApiService> { onBlocking { userDetails(1) } doAnswer { if (throwError) throw IOException() else userDetails } } pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } } }
  54. @Test fun `should retry with success`() = dispatcher.runBlockingTest { val

    apiService = mock<ApiService> { onBlocking { userDetails(1) } doAnswer { if (throwError) throw IOException() else userDetails } } pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } advanceTimeBy(DELAY_ONE_SECOND) throwError = false advanceTimeBy(DELAY_ONE_SECOND) } }
  55. 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) }
  56. @Test fun `should retry with success`() = dispatcher.runBlockingTest { val

    apiService = mock<ApiService> { onBlocking { userDetails(1) } doAnswer { if (throwError) throw IOException() else userDetails } } pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } advanceTimeBy(DELAY_ONE_SECOND) throwError = false advanceTimeBy(DELAY_ONE_SECOND) } }
  57. @Test fun `should retry with success`() = dispatcher.runBlockingTest { val

    apiService = mock<ApiService> { onBlocking { userDetails(1) } doAnswer { if (throwError) throw IOException() else userDetails } } pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } advanceTimeBy(DELAY_ONE_SECOND) throwError = false advanceTimeBy(DELAY_ONE_SECOND) } }
  58. @Test fun `should retry with success`() = dispatcher.runBlockingTest { val

    apiService = mock<ApiService> { onBlocking { userDetails(1) } doAnswer { if (throwError) throw IOException() else userDetails } } pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } advanceTimeBy(DELAY_ONE_SECOND) throwError = false advanceTimeBy(DELAY_ONE_SECOND) } }
  59. 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) }
  60. @Test fun `should retry with success`() = dispatcher.runBlockingTest { val

    apiService = mock<ApiService> { onBlocking { userDetails(1) } doAnswer { if (throwError) throw IOException() else userDetails } } pauseDispatcher { val flow = repository.getUserDetails(id = 1) launch { flow.collect { it.isSuccess.assertTrue() } } advanceTimeBy(DELAY_ONE_SECOND) throwError = false advanceTimeBy(DELAY_ONE_SECOND) } }
  61. 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) } } }
  62. class CoroutineTestRule( val dispatcher= TestCoroutineDispatcher() ) : TestWatcher() { override

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

    fun finished(description: Description?) { super.finished(description) Dispatchers.resetMain() dispatcher.cleanupTestCoroutines() } }
  64. @get:Rule val rule = CoroutineTestRule() val repository = mock<UserRepository>() val

    stateManager = mock<StateManager>() val viewModel = UserDetailsViewModel(repository, stateManager)
  65. 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) } } }
  66. @Test fun `should dispatch details`() = rule.dispatcher.runBlockingTest { val userDetails

    = UserDetails(1, "User 1", "avatar") val result = Result.success(userDetails) }
  67. @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 } Flow Channel Convert to Flow
  68. @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 launch { channel.send(result) } } Flow Channel Send Producer to send values
  69. @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 launch { channel.send(result) } userDetailsViewModel.getUserDetails() verify(stateManager).dispatch(result) }
  70. @Test fun `should get users details on success`() = runBlocking

    { ... flow.collect { } } RxJava Observable.test()
  71. @Test fun `should get users details on success`() = runBlocking

    { ... flow.test { expectItem() assertEquals userDetails expectComplete() } }
  72. • Channels & Flow in Practice https://speakerdeck.com/heyitsmohit/channels-and-flows-in-practice • Dissecting Coroutines

    https://speakerdeck.com/heyitsmohit/dissecting-coroutines • Koltin Channels Under the Hood https://medium.com/@heyitsmohit/kotlin-coroutine-channels-under-the-hood-part-1