Slide 1

Slide 1 text

Mohit Sarveiya, Android & Kotlin Google Dev Expert Unit Testing Kotlin Channels & Flows @heyitsmohit

Slide 2

Slide 2 text

Unit Testing Channels & Flows ● Testing Flows ● Errors ● Delays ● View Model Test ● Common Testing Problems 
 


Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

Flow of locations

Slide 6

Slide 6 text

flowOfLocations: Flow .filter { it.locationType == LocationType.RESTAURANT } .map { it.name }

Slide 7

Slide 7 text

flowOfLocations: Flow .filter { ... } .map { ... } .flatmap { ... }

Slide 8

Slide 8 text

flowOfLocations: Flow .filter { ... } .map { ... } .flatmap { ... } How do we test this flow?

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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’ }

Slide 12

Slide 12 text

@Test fun `should get locations`() { }

Slide 13

Slide 13 text

@Test fun `should get locations`() = runBlockingTest { } Creates a coroutine

Slide 14

Slide 14 text

@Test fun `should get locations`() = runBlockingTest { } Test Scope

Slide 15

Slide 15 text

@Test fun `should get locations`() = runBlockingTest { } Test Scope + Test Dispatcher

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

@Test fun `should get locations`() = runBlockingTest { } Test Scope + Test Dispatcher

Slide 18

Slide 18 text

@Test fun `should get locations`() = runBlockingTest { } Test Scope + Test Dispatcher

Slide 19

Slide 19 text

flowOfLocations: Flow .filter { ... } .map { ... } .flatmap { ... }

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

@Test fun `should get locations`() = runBlockingTest { val locations: List = flowOfLocations.toList() locations shouldContainAll listOf(location1, location2, location3) } How does this work?

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

@Test fun `should get locations`() = runBlockingTest { val flowOfLocations = mockFlow() val locations: List = flowOfLocations.toList() locations shouldContainAll listOf(location1, location2, location3) } Better way to collect from Flows?

Slide 35

Slide 35 text

cashApp/turbine Code ! Issues Pull Requests Turbine Small testing library for kotlinx.coroutines Flow. testImplementation ‘app.cash.turbine:turbine:x.x.x'

Slide 36

Slide 36 text

Flow to test How Turbine Works

Slide 37

Slide 37 text

Channel (Unlimited) How Turbine Works

Slide 38

Slide 38 text

Channel Send Receive

Slide 39

Slide 39 text

Channel Types ● Unlimited ● Buffered ● Rendezvous ● Broadcast Channel [Deprecated] 
 


Slide 40

Slide 40 text

How Turbine Works Channel (Unlimited) Event

Slide 41

Slide 41 text

sealed class Event { 
 object Complete 
 data class Error( ... ) 
 data class Item(val value: T) 
 } How Turbine Works Channel (Unlimited) Event

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

object Complete: Event() How Turbine Works Channel (Unlimited) How do I query items in channels?

Slide 48

Slide 48 text

interface FlowTurbine { val timeout: Duration fun expectNoEvents() suspend fun expectItem(): T fun expectError(): Throwable suspend fun expectComplete() } API to query Channel How Turbine Works

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

interface FlowTurbine { val timeout: Duration fun expectNoEvents() suspend fun expectItem(): T fun expectError(): Throwable suspend fun expectComplete() } How Turbine Works

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

Channel (Unlimited) Testing With Turbine Location Data

Slide 62

Slide 62 text

Channel (Unlimited) Testing With Turbine Location Data

Slide 63

Slide 63 text

Channel (Unlimited) Testing With Turbine Query Channel in test

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

Channel (Unlimited) Testing With Turbine Verify Flow completed

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

Channel (Unlimited) Testing With Turbine Miss verifying emission?

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

@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(…))

Slide 74

Slide 74 text

Channel (Unlimited) Testing With Turbine Miss a Flow completed

Slide 75

Slide 75 text

@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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

Unit Testing Channels & Flows ● Testing Flows ● Errors ● Delays ● View Model Test ● Common Testing Problems 
 


Slide 78

Slide 78 text

flowOfLocations: Flow .filter { ... } .map { ... } .flatMap { ... } Exception

Slide 79

Slide 79 text

flowOfLocations: Flow .filter { ... } .map { ... } .flatMap { ... } .onCompletion { ... } Complete on error

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

How Turbine Works suspend fun Flow.test( ... ) { coroutineScope { val events = Channel> (UNLIMITED) 
 launch { try { collect { ... } } catch { events.send(Event.Error( ... )) } } Send error to Channel

Slide 82

Slide 82 text

@Test fun `should handle exception`() = runBlockingTest { flowOfLocations.test { expectError() } 
 } Throwable

Slide 83

Slide 83 text

flowOfLocations: Flow .filter { ... } .map { ... } .flatMap { ... } Better way to 
 handle Exception?

Slide 84

Slide 84 text

flowOfLocations: Flow .filter { ... } .map { ... } .flatMap { ... } .catch { . .. } Catch Exception

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

Code ! Issues Pull Requests Jetbrains/kotlin stdlib/src/kotlin/util/Result.kt inline class Result(val value: Any?) { val isSuccess: Boolean val isFailure: Boolean }

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

flowOfLocations .filter { ... } .map { ... } .flatMap { ... } .catch { . .. } Go to Catch when exception occurs

Slide 90

Slide 90 text

flowOfLocations .collect { result - > result.isFailure }

Slide 91

Slide 91 text

Error stored as item data class Item(throwable): Event() How Turbine Works Channel (Unlimited)

Slide 92

Slide 92 text

@Test fun `should handle exception`() = runBlockingTest { flowOfLocations.test { expectItem().isFailure } 
 } Verify Failure

Slide 93

Slide 93 text

Unit Testing Channels & Flows ● Testing Flows ● Errors ● Delays ● View Model Test ● Common Testing Problems 
 


Slide 94

Slide 94 text

Testing Delays ● Delays in suspending methods ● Delays in coroutines ● Delays in Flows 
 


Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

@Test fun `should handle data updates`() = runBlockingTest { processData() } suspend fun processData() { delay(1000) calculate() } How does this work in a test?

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

@Test fun `should handle data updates`() = runBlockingTest { processData() } suspend fun processData() { delay(1000) calculate() } Auto advances time forward

Slide 101

Slide 101 text

Testing Delays ● Delays in suspending methods ● Delays in coroutines ● Delays in Flows 
 


Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

interface DelayController { fun advanceTimeBy(delayTimeMillis: Long) fun advanceUntilIdle(): Long fun pauseDispatcher() fun runCurrent() } Delay Utilities

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

Testing Delays ● Delays in suspending methods ● Delays in coroutines ● Delays in Flows 
 


Slide 117

Slide 117 text

flowOfLocations: Flow .filter { ... } .map { ... } .flatmap { ... } Exception

Slide 118

Slide 118 text

flowOfLocations: Flow .filter { ... } .map { ... } .flatmap { ... } Retry?

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

@Test fun `should retry twice`() = runBlockingTest { }

Slide 126

Slide 126 text

val testCoroutineDispatcher = TestCoroutineDispatcher() 
 @Test fun `should retry twice`() = runBlockingTest { }

Slide 127

Slide 127 text

flowOfLocations: Flow .filter { ... } .map { ... } .flatmap { ... } .retry(1) { delay(2000) ... } .flowOn(testCoroutineDispatcher) Run on test dispatcher

Slide 128

Slide 128 text

val testCoroutineDispatcher = TestCoroutineDispatcher() 
 @Test fun `should retry twice`() = testCoroutineDispatcher.runBlockingTest { } Use dispatcher in test

Slide 129

Slide 129 text

fun TestCoroutineDispatcher.runBlockingTest( block: suspend TestCoroutineScope.() -> Unit ) = runBlockingTest(this, block) Extension on dispatcher

Slide 130

Slide 130 text

@Test fun `should retry twice`() = testCoroutineDispatcher.runBlockingTest { } flowOfLocations.test { } Start Collecting from Flow

Slide 131

Slide 131 text

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

Slide 132

Slide 132 text

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

Slide 133

Slide 133 text

flowOfLocations: Flow .filter { ... } .map { ... } .flatmap { ... } .retry(1) { delay(2000) ... } .flowOn(dispatcher) Delay

Slide 134

Slide 134 text

@Test fun `should retry twice`() = testCoroutineDispatcher.runBlockingTest { } flowOfLocations.test { advanceTimeBy(2000) // perform assertions } Advance virtual time

Slide 135

Slide 135 text

flowOfLocations: Flow .filter { ... } .map { ... } .flatmap { ... } .retry(1) { delay(2000) ... } .flowOn(dispatcher) Retry Flow

Slide 136

Slide 136 text

@Test fun `should retry twice`() = testCoroutineDispatcher.runBlockingTest { } flowOfLocations.test { advanceTimeBy(2000) // perform assertions } Perform assertion

Slide 137

Slide 137 text

Unit Testing Channels & Flows ● Testing Flows ● Errors ● Delays ● View Model Test ● Common Testing Problems 
 


Slide 138

Slide 138 text

View View Model State

Slide 139

Slide 139 text

class MyViewModel(): ViewModel() { } Jetpack View Model

Slide 140

Slide 140 text

val ViewModel.viewModelScope: CoroutineScope get() { return setTagIfAbsent(JOB_KEY, CloseableCoroutineScope( 
 SupervisorJob() + Dispatchers.Main.immediate) 
 ) } Main Dispatcher

Slide 141

Slide 141 text

class MyViewModel(): ViewModel() { val uiState = MutableStateFlow(UIState()) } State Flow

Slide 142

Slide 142 text

class MyViewModel(): ViewModel() { val uiState = MutableStateFlow(UIState()) } Default value

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

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

Slide 145

Slide 145 text

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

Slide 146

Slide 146 text

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

Slide 147

Slide 147 text

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?

Slide 148

Slide 148 text

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

Slide 149

Slide 149 text

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

Slide 150

Slide 150 text

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

Slide 151

Slide 151 text

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

Slide 152

Slide 152 text

@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

Slide 153

Slide 153 text

val ViewModel.viewModelScope: CoroutineScope get() { return setTagIfAbsent(JOB_KEY, CloseableCoroutineScope( 
 SupervisorJob() + Dispatchers.Main.immediate) 
 ) } Main Dispatcher

Slide 154

Slide 154 text

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

Slide 155

Slide 155 text

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

Slide 156

Slide 156 text

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

Slide 157

Slide 157 text

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

Slide 158

Slide 158 text

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

Slide 159

Slide 159 text

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

Slide 160

Slide 160 text

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

Slide 161

Slide 161 text

@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

Slide 162

Slide 162 text

https: // developer.android.com/kotlin/flow/stateflow-and-sharedflow State Flow

Slide 163

Slide 163 text

Unit Testing Channels & Flows ● Testing Flows ● Errors ● Delays ● View Model Test ● Common Testing Problems 
 


Slide 164

Slide 164 text

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

Slide 165

Slide 165 text

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

Slide 166

Slide 166 text

Active Job Causes ● Coroutine is Suspended ● Delays

Slide 167

Slide 167 text

val channel = Channel() Active Jobs Rendezvous Channel

Slide 168

Slide 168 text

val channel = Channel() Active Jobs Send Receive

Slide 169

Slide 169 text

val channel = Channel() Active Jobs fun CoroutineScope.channelSend() { launch { channel.send(Event()) } .. . } Send events

Slide 170

Slide 170 text

val channel = Channel() Active Jobs suspend fun processEvents() { channel.consumeAsFlow().collect { event -> ... } } Process event

Slide 171

Slide 171 text

Active Jobs @Test fun `should process event`() = runBlockingTest { sendEvent() } Coroutine

Slide 172

Slide 172 text

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

Slide 173

Slide 173 text

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

Slide 174

Slide 174 text

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

Slide 175

Slide 175 text

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}]

Slide 176

Slide 176 text

Active Jobs @Test fun `should process event`() = runBlockingTest { sendEvent() processEvents() }

Slide 177

Slide 177 text

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

Slide 178

Slide 178 text

@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}]

Slide 179

Slide 179 text

Delay Active Jobs @Test fun `should process event`() = runBlockingTest { sendEvent() testCoroutineDispatcher.advanceTimeBy(1000) } suspend fun CoroutineScope.sendEvent() { launch(testCoroutineDispatcher) { delay(1000) ... } } Advance virtual time

Slide 180

Slide 180 text

Testing inside Coroutines Lib ● Learn Coroutines from tests inside lib ● Multiplatform testing - JS, JVM and Native 
 


Slide 181

Slide 181 text

@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) }

Slide 182

Slide 182 text

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

Slide 183

Slide 183 text

class JobBasicCancellationTest : TestBase() { @Test fun testCancelJobImpl() { } } Test parent job is not cancelled

Slide 184

Slide 184 text

@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) }

Slide 185

Slide 185 text

@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

Slide 186

Slide 186 text

@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

Slide 187

Slide 187 text

@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

Slide 188

Slide 188 text

@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

Slide 189

Slide 189 text

@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) }

Slide 190

Slide 190 text

expect open class TestBase { fun error(message, cause) fun expect(index: Int) fun finish(index: Int) fun runTest( block: suspend CoroutineScope.() -> Unit ) }

Slide 191

Slide 191 text

JVM JS Native Common TestBase.kt TestBase.kt TestBase.kt TestBase.kt

Slide 192

Slide 192 text

actual fun runTest( block: suspend CoroutineScope.() - > Unit ) { runBlocking( block = block, context = CoroutineExceptionHandler { ... }) 
 }

Slide 193

Slide 193 text

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?

Slide 194

Slide 194 text

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" } }

Slide 195

Slide 195 text

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" } }

Slide 196

Slide 196 text

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" } }

Slide 197

Slide 197 text

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?

Slide 198

Slide 198 text

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

Slide 199

Slide 199 text

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

Slide 200

Slide 200 text

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

Slide 201

Slide 201 text

@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) }

Slide 202

Slide 202 text

https: // codingwithmohit.com/coroutines/unit-testing-delays-errors-retries-with-kotlin-flows/ Unit Testing

Slide 203

Slide 203 text

Kotlin Coroutines Training • Groups or Individual Sessions • Contact for booking [email protected]

Slide 204

Slide 204 text

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/

Slide 205

Slide 205 text

Thank You! @heyitsmohit www.codingwithmohit.com