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

Unit Testing Channels & Flows - Droidcon Americas

Mohit S
November 17, 2020

Unit Testing Channels & Flows - Droidcon Americas

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

November 17, 2020
Tweet

More Decks by Mohit S

Other Decks in Technology

Transcript

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

    • Delays • Dispatcher Utilities • Common Testing Problems • Testing inside Coroutines Lib
  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:1.4.1' }
  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() } Mockito or create Fake
  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:0.2.1'
  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 {

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

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

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

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

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

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

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

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

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

    expectItem() shouldBeEqualTo location1 expectItem() shouldBeEqualTo location2 expectComplete() } } Missed varying location 3
  43. @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(…))
  44. @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
  45. @Test fun `should get locations`() = runBlockingTest { flowOfLocations.test {

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

    • Delays • Dispatcher Utilities • Common Testing Problems • Testing inside Coroutines Lib
  47. flowOfLocations: Flow<Location> .filter { ""... } .map { ""... }

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

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

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

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

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

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

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

    • Delays • Dispatcher Utilities • Common Testing Problems • Testing inside Coroutines Lib
  56. @Test fun `should handle data updates`() = runBlockingTest { processData()

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

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

    } suspend fun processData() { delay(1000) calculate() } How does this work in a test?
  59. 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
  60. 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
  61. @Test fun `should handle data updates`() = runBlockingTest { processData()

    } suspend fun processData() { delay(1000) calculate() } Advance virtual time until idle
  62. @Test fun `should handle data updates`() = runBlockingTest { processData()

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

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

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

    performAssertions() } suspend fun CoroutineScope.processData() { launch { delay(1000) calculate() } } Continue execution
  66. 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
  67. @Test fun `should handle data updates`() = runBlockingTest { processData()

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

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

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

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

    advanceTimeBy(1000) performAssertions() } suspend fun CoroutineScope.processData() { launch { delay(1000) calculate() } } Perform assertions
  72. flowOfLocations: Flow<Location> .filter { ""... } .map { ""... }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    • Delays • Dispatcher Utilities • Common Testing Problems • Testing inside Coroutines Lib
  87. class MyViewModel(): ViewModel() { val states = MutableStateFlow(State()) fun updateState()

    { flowOfData .map { mapToState(data) } .onEach { state "-> states.value = state } .launchIn(viewModelScope) } }
  88. class MyViewModel(): ViewModel() { val states = MutableStateFlow(State()) fun updateState()

    { flowOfData .map { mapToState(data) } .onEach { state "-> states.value = state } .launchIn(viewModelScope) } }
  89. class MyViewModel(): ViewModel() { val states = MutableStateFlow(State()) fun updateState()

    { flowOfData .map { mapToState(data) } .onEach { state "-> states.value = state } .launchIn(viewModelScope) } }
  90. class MyViewModel(): ViewModel() { val states = MutableStateFlow(State()) fun updateState()

    { flowOfData .map { mapToState(data) } .collect { state "-> states.value = state } "// logic after collect will run after it finishes } } Suspending
  91. class MyViewModel(): ViewModel() { val states = MutableStateFlow(State()) fun updateState()

    { flowOfData .map { mapToState(data) } .onEach { state "-> states.value = state } .launchIn(viewModelScope) } } How do we test this flow?
  92. @Test fun `should get data`() = runBlockingTest { mockDataFlow() viewModel.updateState()

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

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

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

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

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

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

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

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

    = runBlockingTest { mockDataFlow() viewModel.updateState() viewModel.states.test { expectItem().data shouldBe state } }
  102. @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
  103. Unit Testing Channels & Flows • Testing Flows • Errors

    • Delays • Dispatcher Utilities • Common Testing Problems • Testing inside Coroutines Lib
  104. 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
  105. 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
  106. val channel = Channel<Event>() Active Jobs suspend fun processEvents() {

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

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

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

    sendEvent() } channel.send(LocationEvent()) Coroutine SUSPENDED
  110. 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}]
  111. Delay Active Jobs @Test fun `should process event`() = runBlockingTest

    { sendEvent() } suspend fun CoroutineScope.sendEvent() { launch(testCoroutineDispatcher) { delay(1000) ""... } } Delay
  112. @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}]
  113. Delay Active Jobs @Test fun `should process event`() = runBlockingTest

    { sendEvent() testCoroutineDispatcher.advanceTimeBy(1000) } suspend fun CoroutineScope.sendEvent() { launch(testCoroutineDispatcher) { delay(1000) ""... } } Advance virtual time
  114. Unit Testing Channels & Flows • Testing Flows • Errors

    • Delays • Dispatcher Utilities • Common Testing Problems • Testing inside Coroutines Lib
  115. Testing inside Coroutines Lib • Learn Coroutines from tests inside

    lib • Multiplatform testing - JS, JVM and Native
  116. @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) }
  117. @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) }
  118. @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
  119. @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
  120. @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
  121. @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
  122. @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) }
  123. expect open class TestBase { fun error(message, cause) fun expect(index:

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

    runBlocking( block = block, context = CoroutineExceptionHandler { ""... }) }
  125. 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?
  126. 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" } }
  127. 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" } }
  128. 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" } }
  129. 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?
  130. private var finished = AtomicBoolean() actual fun finish(index: Int) {

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

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

    expect(index) check(!finished.getAndSet(true)) { "Should call 'finish(""...)' at most once" } }
  133. @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) }
  134. 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/