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

Unit Testing Channels & Flows [Android Worldwide]

Mohit S
April 27, 2021

Unit Testing Channels & Flows [Android Worldwide]

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.

Mohit S

April 27, 2021
Tweet

More Decks by Mohit S

Other Decks in Programming

Transcript

  1. Unit Testing Channels & Flows • Testing Flows • Errors

    • Delays • View Model Test • Common Testing Problems 
 

  2. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatmap { ... } How do we test this flow?
  3. 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
  4. 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
  5. 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:x.x.x’ }
  6. @Test fun `should get locations`() = runBlockingTest { } Test

    Scope + Test Dispatcher What is executed in this coroutine?
  7. @Test fun `should get locations`() = runBlockingTest { val flowOfLocations

    = mockFlow() val locations: List<Location> = flowOfLocations.toList() } Create Mock Flow
  8. @Test fun `should get locations`() = runBlockingTest { val flowOfLocations

    = mockFlow() val locations: List<Location> = flowOfLocations.toList() }
  9. @Test fun `should get locations`() = runBlockingTest { val locations:

    List<Location> = flowOfLocations.toList() }
  10. @Test fun `should get locations`() = runBlockingTest { val locations:

    List<Location> = flowOfLocations.toList() }
  11. @Test fun `should get locations`() = runBlockingTest { val locations:

    List<Location> = flowOfLocations.toList() }
  12. @Test fun `should get locations`() = runBlockingTest { val locations:

    List<Location> = flowOfLocations.toList() }
  13. @Test fun `should get locations`() = runBlockingTest { val locations:

    List<Location> = flowOfLocations.toList() locations shouldContainAll listOf(location1, location2, location3) }
  14. @Test fun `should get locations`() = runBlockingTest { val flowOfLocations

    = mockFlow() val locations: List<Location> = flowOfLocations.toList() locations shouldContainAll listOf(location1, location2, location3) }
  15. @Test fun `should get locations`() = runBlockingTest { val locations:

    List<Location> = flowOfLocations.toList() locations shouldContainAll listOf(location1, location2, location3) } How does this work?
  16. 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
  17. fun runBlockingTest(testBody) { val scope = TestCoroutineScope(safeContext) 
 
 val

    deferred = scope.async { scope.testBody() } dispatcher.advanceUntilIdle() deferred.getCompletionExceptionOrNull() ?. let { throw it } 
 
 scope.cleanupTestCoroutines() if (activeJobs()).isNotEmpty()) { throw UncompletedCoroutinesError("Test finished with active jobs") } } Setup
  18. fun runBlockingTest(testBody) { val scope = TestCoroutineScope(safeContext) 
 
 val

    deferred = scope.async { scope.testBody() } dispatcher.advanceUntilIdle() deferred.getCompletionExceptionOrNull() ?. let { throw it } 
 
 scope.cleanupTestCoroutines() if (activeJobs()).isNotEmpty()) { throw UncompletedCoroutinesError("Test finished with active jobs") } } Setup Run Test
  19. fun runBlockingTest(testBody) { val scope = TestCoroutineScope(safeContext) 
 
 val

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

    = mockFlow() val locations: List<Location> = flowOfLocations.toList() locations shouldContainAll listOf(location1, location2, location3) }
  21. @Test fun `should get locations`() = runBlockingTest { val flowOfLocations

    = mockFlow() val locations: List<Location> = flowOfLocations.toList() locations shouldContainAll listOf(location1, location2, location3) } Better way to collect from Flows?
  22. cashApp/turbine Code ! Issues Pull Requests Turbine Small testing library

    for kotlinx.coroutines Flow. testImplementation ‘app.cash.turbine:turbine:x.x.x'
  23. sealed class Event<out T> { 
 object Complete 
 data

    class Error( ... ) 
 data class Item(val value: T) 
 } How Turbine Works Channel (Unlimited) Event
  24. interface FlowTurbine<T> { val timeout: Duration fun expectNoEvents() suspend fun

    expectItem(): T fun expectError(): Throwable suspend fun expectComplete() } API to query Channel How Turbine Works
  25. 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
  26. interface FlowTurbine<T> { val timeout: Duration fun expectNoEvents() suspend fun

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

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

    expectItem(): T fun expectError(): Throwable suspend fun expectComplete() } Verify error occurred How Turbine Works
  29. 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
  30. interface FlowTurbine<T> { val timeout: Duration fun expectNoEvents() suspend fun

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

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

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

    coroutineScope { val events = Channel<Event<T >> (UNLIMITED) 
 launch { } }
  34. 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
  35. 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
  36. @Test fun `should get locations`() = runBlockingTest { val locations:

    List<Location> = flowOfLocations.toList() locations shouldContainAll listOf(location1, location2, location3) } How do we use Turbine?
  37. @Test fun `should get locations`() = runBlockingTest { flowOfLocations.test {

    expectItem() shouldBeEqualTo location1 expectItem() shouldBeEqualTo location2 expectItem() shouldBeEqualTo location3 expectComplete() } 
 } Read items into Channel
  38. @Test fun `should get locations`() = runBlockingTest { flowOfLocations.test {

    expectItem() shouldBeEqualTo location1 expectItem() shouldBeEqualTo location2 expectItem() shouldBeEqualTo location3 expectComplete() } 
 } Get first emitted item
  39. @Test fun `should get locations`() = runBlockingTest { flowOfLocations.test {

    expectItem() shouldBeEqualTo location1 expectItem() shouldBeEqualTo location2 expectItem() shouldBeEqualTo location3 expectComplete() } 
 } Assertion
  40. @Test fun `should get locations`() = runBlockingTest { flowOfLocations.test {

    expectItem() shouldBeEqualTo location1 expectItem() shouldBeEqualTo location2 expectItem() shouldBeEqualTo location3 expectComplete() } 
 } Get 1st, 2nd, 3rd item
  41. @Test fun `should get locations`() = runBlockingTest { flowOfLocations.test {

    expectItem() shouldBeEqualTo location1 expectItem() shouldBeEqualTo location2 expectItem() shouldBeEqualTo location3 expectComplete() } 
 } Verify flow completed
  42. @Test fun `should get locations`() = runBlockingTest { flowOfLocations.test {

    expectItem() shouldBeEqualTo location1 expectItem() shouldBeEqualTo location2 expectItem() shouldBeEqualTo location3 expectComplete() } 
 }
  43. @Test fun `should get locations`() = runBlockingTest { flowOfLocations.test {

    expectItem() shouldBeEqualTo location1 expectItem() shouldBeEqualTo location2 expectComplete() } 
 } Missed varying location 3
  44. @Test fun `should get locations`() = runBlockingTest { flowOfLocations.test {

    expectItem() shouldBeEqualTo location1 expectItem() shouldBeEqualTo location2 expectComplete() } 
 } LocationRepoTest.kt Run: LocationRepoTest.kt should get locations app.cash.turbine.AssertionError: Expected complete but found Item(Location(…))
  45. @Test fun `should get locations`() = runBlockingTest { flowOfLocations.test {

    expectItem() shouldBeEqualTo location1 expectItem() shouldBeEqualTo location2 expectItem() shouldBeEqualTo location3 } 
 } LocationRepoTests.kt Run: LocationRepoTests should get locations app.cash.turbine.AssertionError: Unconsumed events found: 
 - Complete
  46. @Test fun `should get locations`() = runBlockingTest { flowOfLocations.test {

    expectItem() shouldBeEqualTo location1 expectItem() shouldBeEqualTo location2 expectItem() shouldBeEqualTo location3 expectComplete() } 
 }
  47. Unit Testing Channels & Flows • Testing Flows • Errors

    • Delays • View Model Test • Common Testing Problems 
 

  48. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatMap { ... } .onCompletion { ... } Complete on error
  49. 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
  50. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatMap { ... } Better way to 
 handle Exception?
  51. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatMap { ... } .catch { . .. } Catch Exception
  52. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatMap { ... } .catch { this: FlowCollector } Allow you to emit from catch
  53. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatMap { ... } .catch { throwable -> } Exception
  54. flowOfLocations: Flow<Result> .filter { ... } .map { ... }

    .flatMap { ... } .catch { throwable -> emit(Result.failure(throwable)) } Emit result
  55. flowOfLocations .filter { ... } .map { ... } .flatMap

    { ... } .catch { . .. } Go to Catch when exception occurs
  56. Unit Testing Channels & Flows • Testing Flows • Errors

    • Delays • View Model Test • Common Testing Problems 
 

  57. Testing Delays • Delays in suspending methods • Delays in

    coroutines • Delays in Flows 
 

  58. @Test fun `should handle data updates`() = runBlockingTest { processData()

    } suspend fun processData() { delay(1000) calculate() }
  59. @Test fun `should handle data updates`() = runBlockingTest { processData()

    } suspend fun processData() { delay(1000) calculate() }
  60. @Test fun `should handle data updates`() = runBlockingTest { processData()

    } suspend fun processData() { delay(1000) calculate() } How does this work in a test?
  61. fun runBlockingTest(testBody) { val scope = TestCoroutineScope(safeContext) 
 
 val

    deferred = scope.async { scope.testBody() } dispatcher.advanceUntilIdle() deferred.getCompletionExceptionOrNull() ?. let { throw it } 
 
 scope.cleanupTestCoroutines() if (activeJobs()).isNotEmpty()) { throw UncompletedCoroutinesError("Test finished with active jobs") } } Setup Run Test Cleanup
  62. fun runBlockingTest(testBody) { val scope = TestCoroutineScope(safeContext) 
 
 val

    deferred = scope.async { scope.testBody() } dispatcher.advanceUntilIdle() deferred.getCompletionExceptionOrNull() ?. let { throw it } 
 
 scope.cleanupTestCoroutines() if (activeJobs()).isNotEmpty()) { throw UncompletedCoroutinesError("Test finished with active jobs") } } Run Test Advance virtual time
  63. @Test fun `should handle data updates`() = runBlockingTest { processData()

    } suspend fun processData() { delay(1000) calculate() } Auto advances time forward
  64. Testing Delays • Delays in suspending methods • Delays in

    coroutines • Delays in Flows 
 

  65. @Test fun `should handle data updates`() = runBlockingTest { processData()

    performAssertions() } suspend fun CoroutineScope.processData() { launch { delay(1000) calculate() } } Creating a new coroutine
  66. @Test fun `should handle data updates`() = runBlockingTest { processData()

    performAssertions() } suspend fun CoroutineScope.processData() { launch { delay(1000) calculate() } } Delay C
  67. @Test fun `should handle data updates`() = runBlockingTest { processData()

    performAssertions() } suspend fun CoroutineScope.processData() { launch { delay(1000) calculate() } } Continue
  68. @Test fun `should handle data updates`() = runBlockingTest { processData()

    performAssertions() } suspend fun CoroutineScope.processData() { launch { delay(1000) calculate() } } Outside of coroutine
  69. @Test fun `should handle data updates`() = runBlockingTest { processData()

    performAssertions() } suspend fun CoroutineScope.processData() { launch { delay(1000) calculate() } } How is the delay handled?
  70. @Test fun `should handle data updates`() = runBlockingTest { processData()

    performAssertions() } suspend fun CoroutineScope.processData() { launch { delay(1000) calculate() } } SUSPEND
  71. @Test fun `should handle data updates`() = runBlockingTest { processData()

    performAssertions() } suspend fun CoroutineScope.processData() { launch { delay(1000) calculate() } } Continue execution
  72. fun runBlockingTest(testBody) { val scope = TestCoroutineScope(safeContext) 
 
 val

    deferred = scope.async { scope.testBody() } dispatcher.advanceUntilIdle() deferred.getCompletionExceptionOrNull() ?. let { throw it } 
 
 scope.cleanupTestCoroutines() if (activeJobs()).isNotEmpty()) { throw UncompletedCoroutinesError("Test finished with active jobs") } } Run Test Advance virtual time
  73. @Test fun `should handle data updates`() = runBlockingTest { processData()

    performAssertions() } suspend fun CoroutineScope.processData() { launch { delay(1000) calculate() } } Proceed forward to calculate
  74. @Test fun `should handle data updates`() = runBlockingTest { processData()

    performAssertions() } suspend fun CoroutineScope.processData() { launch { delay(1000) calculate() } } How do I control this delay?
  75. @Test fun `should handle data updates`() = runBlockingTest { processData()

    advanceTimeBy(1000) performAssertions() } suspend fun CoroutineScope.processData() { launch { delay(1000) calculate() } } Advance virtual time
  76. @Test fun `should handle data updates`() = runBlockingTest { processData()

    advanceTimeBy(1000) performAssertions() } suspend fun CoroutineScope.processData() { launch { delay(1000) calculate() } } Perform calculation
  77. @Test fun `should handle data updates`() = runBlockingTest { processData()

    advanceTimeBy(1000) performAssertions() } suspend fun CoroutineScope.processData() { launch { delay(1000) calculate() } } Perform assertions
  78. Testing Delays • Delays in suspending methods • Delays in

    coroutines • Delays in Flows 
 

  79. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatmap { ... } .retry { delay(2000) ... } Retry extension
  80. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatmap { ... } .retry(1) { delay(2000) ... } # of retries
  81. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatmap { ... } .retry(1) { delay(2000) ... } Delay before retrying
  82. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatmap { ... } .retry(1) { delay(2000) ... } Retry flow operators
  83. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatmap { ... } .retry(1) { delay(2000) ... } .flowOn(dispatcher) Flow on Dispatcher Run on Dispatcher
  84. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatmap { ... } .retry(1) { delay(2000) ... } .flowOn(dispatcher) How do you test this?
  85. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatmap { ... } .retry(1) { delay(2000) ... } .flowOn(testCoroutineDispatcher) Run on test dispatcher
  86. val testCoroutineDispatcher = TestCoroutineDispatcher() 
 @Test fun `should retry twice`()

    = testCoroutineDispatcher.runBlockingTest { } Use dispatcher in test
  87. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatmap { ... } .retry(1) { delay(2000) ... } .flowOn(dispatcher) Call operators
  88. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatmap { ... } .retry(1) { delay(2000) ... } .flowOn(dispatcher) Exception
  89. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatmap { ... } .retry(1) { delay(2000) ... } .flowOn(dispatcher) Delay
  90. @Test fun `should retry twice`() = testCoroutineDispatcher.runBlockingTest { } flowOfLocations.test

    { advanceTimeBy(2000) // perform assertions } Advance virtual time
  91. flowOfLocations: Flow<Location> .filter { ... } .map { ... }

    .flatmap { ... } .retry(1) { delay(2000) ... } .flowOn(dispatcher) Retry Flow
  92. @Test fun `should retry twice`() = testCoroutineDispatcher.runBlockingTest { } flowOfLocations.test

    { advanceTimeBy(2000) // perform assertions } Perform assertion
  93. Unit Testing Channels & Flows • Testing Flows • Errors

    • Delays • View Model Test • Common Testing Problems 
 

  94. class MyViewModel(): ViewModel() { val uiState = MutableStateFlow(UIState()) fun getData()

    { 
 viewModelScope.launchWhenStarted { newsRepository.getData() } } }
  95. class MyViewModel(): ViewModel() { val uiState = MutableStateFlow(UIState()) fun getData()

    { 
 viewModelScope.launchWhenStarted { newsRepository.getData() . collect { } } } }
  96. class MyViewModel(): ViewModel() { val uiState = MutableStateFlow(UIState()) fun getData()

    { 
 viewModelScope.launchWhenStarted { newsRepository.getData() . collect { uiState.value = UiState.Success(it) } } } }
  97. class MyViewModel(): ViewModel() { val uiState = MutableStateFlow(UIState()) fun getData()

    { 
 viewModelScope.launchWhenStarted { newsRepository.getData() . collect { uiState.value = UiState.Success(it) } } } } How do we test this flow?
  98. @Test fun `should get data`() = runBlockingTest { 
 mockDataFlow()

    
 viewModel.states.test { expectItem().data shouldBe state } }
  99. @Test fun `should get data`() = runBlockingTest { 
 mockDataFlow()

    
 viewModel.getData() viewModel.states.test { expectItem().data shouldBe state } }
  100. @Test fun `should get data`() = runBlockingTest { 
 mockDataFlow()

    
 viewModel.getData() viewModel.uiState.test { } }
  101. @Test fun `should get data`() = runBlockingTest { 
 mockDataFlow()

    
 viewModel.getData() viewModel.uiState.test { expectItem() shouldBe UIState.Success( ... ) } }
  102. @Test fun `should get data`() = runBlockingTest { 
 mockDataFlow()

    
 viewModel.updateState() viewModel.states.test { expectItem().data shouldBe state } } 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
  103. val dispatcher = TestCoroutineDispatcher() @Before fun setUp() { Dispatchers.setMain(dispatcher) }

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

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

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

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

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

    = runBlockingTest { 
 
 mockDataFlow() 
 viewModel.updateState() viewModel.states.test { expectItem().data shouldBe state } }
  109. @get:Rule val testRule = CoroutineTestRule() @Test fun `should get data`()

    = runBlockingTest { 
 
 mockDataFlow() 
 viewModel.getData() viewModel.uiStates.test { expectItem() shouldBe UIState.Success( ... ) } }
  110. @get:Rule val testRule = CoroutineTestRule() @Test fun `should get data`()

    = runBlockingTest { 
 
 mockDataFlow() 
 viewModel.updateState() viewModel.states.test { expectItem().data shouldBe state } } ViewModelTest.kt Run: ViewModelTest should get data
  111. Unit Testing Channels & Flows • Testing Flows • Errors

    • Delays • View Model Test • Common Testing Problems 
 

  112. fun runBlockingTest(testBody) { val scope = TestCoroutineScope(safeContext) 
 
 val

    deferred = scope.async { scope.testBody() } dispatcher.advanceUntilIdle() deferred.getCompletionExceptionOrNull() ?. let { throw it } 
 
 scope.cleanupTestCoroutines() if (activeJobs()).isNotEmpty()) { throw UncompletedCoroutinesError("Test finished with active jobs") } } Setup Run Test Cleanup
  113. fun runBlockingTest(testBody) { val scope = TestCoroutineScope(safeContext) 
 
 val

    deferred = scope.async { scope.testBody() } dispatcher.advanceUntilIdle() deferred.getCompletionExceptionOrNull() ?. let { throw it } 
 
 scope.cleanupTestCoroutines() if (activeJobs()).isNotEmpty()) { throw UncompletedCoroutinesError("Test finished with active jobs") } } Cleanup Job is Active
  114. val channel = Channel<Event>() Active Jobs suspend fun processEvents() {

    channel.consumeAsFlow().collect { event -> ... } } Process event
  115. Active Jobs @Test fun `should process event`() = runBlockingTest {

    sendEvent() } channel.send(LocationEvent()) Coroutine
  116. Active Jobs @Test fun `should process event`() = runBlockingTest {

    sendEvent() } channel.send(LocationEvent()) Coroutine Rendezvous Channel
  117. Active Jobs @Test fun `should process event`() = runBlockingTest {

    sendEvent() } channel.send(LocationEvent()) Coroutine SUSPENDED
  118. Active Jobs @Test fun `should process event`() = runBlockingTest {

    sendEvent() } Tests.kt Run: Tests should process event Test finished with active jobs: ["Coroutine#2":StandaloneCoroutine{Active}]
  119. Delay Active Jobs @Test fun `should process event`() = runBlockingTest

    { sendEvent() } suspend fun CoroutineScope.sendEvent() { launch(testCoroutineDispatcher) { delay(1000) ... } } Delay
  120. @Test fun `should process event`() = runBlockingTest { sendEvent() }

    suspend fun CoroutineScope.sendEvent() { launch(testCoroutineDispatcher) { delay(1000) ... } } Delay Active Jobs Tests.kt Run: Tests should process event Test finished with active jobs: ["Coroutine#2":StandaloneCoroutine{Active}]
  121. Delay Active Jobs @Test fun `should process event`() = runBlockingTest

    { sendEvent() testCoroutineDispatcher.advanceTimeBy(1000) } suspend fun CoroutineScope.sendEvent() { launch(testCoroutineDispatcher) { delay(1000) ... } } Advance virtual time
  122. Testing inside Coroutines Lib • Learn Coroutines from tests inside

    lib • Multiplatform testing - JS, JVM and Native 
 

  123. @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) }
  124. @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) }
  125. @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
  126. @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
  127. @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
  128. @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
  129. @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) }
  130. expect open class TestBase { fun error(message, cause) fun expect(index:

    Int) fun finish(index: Int) fun runTest( block: suspend CoroutineScope.() -> Unit ) }
  131. actual fun runTest( block: suspend CoroutineScope.() - > Unit )

    { runBlocking( block = block, context = CoroutineExceptionHandler { ... }) 
 }
  132. 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?
  133. 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" } }
  134. 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" } }
  135. 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" } }
  136. 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?
  137. private var finished = AtomicBoolean() actual fun finish(index: Int) {

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

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

    expect(index) check(!finished.getAndSet(true)) { "Should call 'finish( ... )' at most once" } }
  140. @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) }
  141. Resources • Unit Testing Delays, Errors & Retries with Kotlin

    Flows 
 
 https: // codingwithmohit.com/coroutines/unit-testing-delays- 
 errors-retries-with-kotlin-flows/ • Multiplatform Testing pattern inside Coroutine Lib 
 
 https: // codingwithmohit.com/coroutines/kotlin-coroutine-lib-testing/