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

Unit Testing Kotlin Flows (Chicago Roboto 2021)

B3f560d34c14a9113e5024bc34ac26a0?s=47 Mohit S
September 27, 2021

Unit Testing Kotlin Flows (Chicago Roboto 2021)

Chicago Roboto 2021

B3f560d34c14a9113e5024bc34ac26a0?s=128

Mohit S

September 27, 2021
Tweet

Transcript

  1. Mohit Sarveiya Unit Testing Kotlin Flows @heyitsmohit

  2. Unit Testing Kotlin Flows • Foundations • Useful Libraries •

    State Flows • Shared Flows
  3. Foundations How run blocking test works

  4. None
  5. None
  6. Flow of data

  7. flowOfData: Flow<Data> .filter { it.locationType == LocationType.RESTAURANT } .map {

    it.name }
  8. flowOfData: Flow<Data> .filter { ... } .map { ... }

    .flatmap { ... }
  9. flowOfData: Flow<Data> .filter { ... } .map { ... }

    .flatmap { ... } How do we test this flow?
  10. Test Cases • Successfully received data

  11. @Test fun `should get data`() { }

  12. @Test fun `should get data`() = runBlockingTest { } Creates

    a coroutine
  13. @Test fun `should get data`() = runBlockingTest { } Test

    Scope
  14. @Test fun `should get data`() = runBlockingTest { } Test

    Scope + Test Dispatcher
  15. @Test fun `should get data`() = runBlockingTest { } Test

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

    Scope + Test Dispatcher
  17. @Test fun `should get data`() = runBlockingTest { } Test

    Scope + Test Dispatcher
  18. flowOfData: Flow<Data> .filter { ... } .map { ... }

    .flatmap { ... }
  19. @Test fun `should get data`() = runBlockingTest { val flowOfData

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

    = mockFlow() val items: List<Data> = flowOfData.toList() }
  21. @Test fun `should get locations`() = runBlockingTest { val items:

    List<Data> = flowOfData.toList() }
  22. @Test fun `should get locations`() = runBlockingTest { val items:

    List<Data> = flowOfData.toList() }
  23. @Test fun `should get locations`() = runBlockingTest { val items:

    List<Data> = flowOfData.toList() }
  24. @Test fun `should get locations`() = runBlockingTest { val items:

    List<Data> = flowOfData.toList() }
  25. @Test fun `should get locations`() = runBlockingTest { val items:

    List<Data> = flowOfData.toList() items shouldContainAll listOf(item1, item2, item3) }
  26. @Test fun `should get data`() = runBlockingTest { val flowOfData

    = mockFlow() val items: List<Data> = flowOfData.toList() items shouldContainAll listOf(item1, item2, item3) }
  27. @Test fun `should get data`() = runBlockingTest { val locations:

    List<Location> = flowOfLocations.toList() locations shouldContainAll listOf(location1, location2, location3) } How does this work?
  28. 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
  29. 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
  30. 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
  31. 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
  32. @Test fun `should get data`() = runBlockingTest { val flowOfData

    = mockFlow() val items: List<Data> = flowOfData.toList() items shouldContainAll listOf(items1, items2, items3) }
  33. Run Blocking Test Different Approaches 1. Use run blocking test

    for all tests 2. Testing Delays -> Use run blocking test 3. Non Delay Uses Cases -> Use run blocking
  34. Useful Libraries Turbine

  35. @Test fun `should get data`() = runBlockingTest { val flowOfLocations

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

    for kotlinx.coroutines Flow. testImplementation ‘app.cash.turbine:turbine:x.x.x'
  37. Test Cases • Verify order of items • Completion •

    Error • Timeouts
  38. What Turbine Provides? suspend fun <T> Flow<T>.test( ... ) coroutineScope

    { val events = Channel<Event<T >> (UNLIMITED) 
 launch { try { collect { ... } } catch { events.send(Event.Error) }
  39. interface FlowTurbine { suspend fun expectItem(): T fun expectNoEvents() fun

    expectError(): Throwable suspend fun expectComplete() } What Turbine Provides?
  40. interface FlowTurbine { suspend fun expectItem(): T fun expectNoEvents() fun

    expectError(): Throwable suspend fun expectComplete() } What Turbine Provides?
  41. interface FlowTurbine { suspend fun expectItem(): T fun expectNoEvents() fun

    expectError(): Throwable suspend fun expectComplete() } What Turbine Provides?
  42. interface FlowTurbine { suspend fun expectItem(): T fun expectNoEvents() fun

    expectError(): Throwable suspend fun expectComplete() } What Turbine Provides?
  43. Test Cases • Verify order of items • Completion •

    Error • Timeouts
  44. @Test fun `should get data`() = runBlockingTest { flowOfData.test {

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

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

    expectItem() shouldBeEqualTo item1 expectItem() shouldBeEqualTo location2 expectItem() shouldBeEqualTo location3 expectComplete() } 
 } Assertion
  47. @Test fun `should get data`() = runBlockingTest { flowOfData.test {

    expectItem() shouldBeEqualTo item1 expectItem() shouldBeEqualTo item2 expectItem() shouldBeEqualTo item3 expectComplete() } 
 } Get 1st, 2nd, 3rd item
  48. @Test fun `should get data`() = runBlockingTest { flowOfData.test {

    expectItem() shouldBeEqualTo item1 expectItem() shouldBeEqualTo item2 } 
 } Missed collecting item
  49. @Test fun `should get data`() = runBlockingTest { flowOfData.test {

    expectItem() shouldBeEqualTo item1 expectItem() shouldBeEqualTo item2 } 
 } RepoTest.kt Run: app.cash.turbine.AssertionError: Expected complete but found Item(Data(…)) DataRepoTests should get data
  50. Test Cases • Verify order of items • Completion •

    Error • Timeouts
  51. @Test fun `should get locations`() = runBlockingTest { flowOfData.test {

    expectItem() shouldBeEqualTo item1 expectItem() shouldBeEqualTo item2 expectItem() shouldBeEqualTo item3 expectComplete() } 
 }
  52. @Test fun `should get locations`() = runBlockingTest { flowOfData.test {

    expectItem() shouldBeEqualTo item1 expectItem() shouldBeEqualTo item2 expectItem() shouldBeEqualTo item3 } 
 } RepoTests.kt Run: DataRepoTests should get data app.cash.turbine.AssertionError: Unconsumed events found: 
 - Complete
  53. @Test fun `should get locations`() = runBlockingTest { flowOfData.test {

    expectItem() shouldBeEqualTo item1 expectItem() shouldBeEqualTo item2 expectItem() shouldBeEqualTo item3 expectComplete() } 
 }
  54. How Turbine Works

  55. Flow to test How Turbine Works

  56. Channel (Unlimited) How Turbine Works

  57. Channel Send Receive

  58. How Turbine Works Channel (Unlimited) Event

  59. sealed class Event<out T> { 
 object Complete 
 data

    class Error( ... ) 
 data class Item(val value: T) 
 } How Turbine Works Channel (Unlimited) Event
  60. data class Item(val value: T): Event() How Turbine Works Channel

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

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

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

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

    Channel (Unlimited)
  65. object Complete: Event() How Turbine Works Channel (Unlimited) How do

    I query items in channels?
  66. 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
  67. interface FlowTurbine<T> { val timeout: Duration fun expectNoEvents() suspend fun

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

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

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

    coroutineScope { val events = Channel<Event<T >> (UNLIMITED) 
 launch { } }
  71. 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
  72. 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
  73. Channel (Unlimited) Testing With Turbine Data

  74. Channel (Unlimited) Testing With Turbine Data

  75. Channel (Unlimited) Testing With Turbine Query Channel in test

  76. Channel (Unlimited) Testing With Turbine Verify Flow completed

  77. @Test fun `should get locations`() = runBlockingTest { flowOfLocations.test {

    expectItem() shouldBeEqualTo location1 expectItem() shouldBeEqualTo location2 expectItem() shouldBeEqualTo location3 expectComplete() } 
 }
  78. Testing State Flows State Flow Use Cases

  79. State Flow Consumer 1 Consumer 2 State State

  80. State Flow View View Model

  81. State Flow View View Model State

  82. State Flow sealed class UiState { 
 data class Error(

    
 val exception: Throwable 
 ): UiState() }
  83. State Flow sealed class UiState { 
 data class Success(

    val data: Data ): UiState() data class Error( 
 val exception: Throwable 
 ): UiState() }
  84. State Flow sealed class UiState { 
 data class Success(

    val data: Data ): UiState() data class Error( 
 val exception: Throwable 
 ): UiState() }
  85. State Flow val stateFlow = MutableStateFlow()

  86. val stateFlow = MutableStateFlow( UiState.Success(Data()) ) State Flow

  87. State Flow val stateFlow = MutableStateFlow(UiState.Success(Data())) @Test fun `should emit

    default value`() = runBlockingTest { stateFlow.test { expectItem() shouldBe UIState.Success } }
  88. State Flow Behavior • Flow never completes normally

  89. State Flow @Test fun `should emit default value`() = runBlockingTest

    { stateFlow.test { expectItem() shouldBe UIState.Success 
 expectComplete() } }
  90. State Flow @Test fun `should emit default value`() = runBlockingTest

    { stateFlow.test { expectItem() shouldBe UIState.Success 
 expectComplete() } } ! Timed out waiting for 1000 ms kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms
  91. State Flow Test Coroutine State Flow

  92. State Flow suspend fun <T> Flow<T>.test( ... ) { coroutineScope

    { val events = Channel<Event<T >> (UNLIMITED) 
 launch { collect { item ->
 events.send(Event.Item(item)) } events.send(Event.Complete) } }
  93. State Flow Behavior • Flow never completes normally • Subscribers

    can complete exceptionally
  94. val stateFlow = MutableStateFlow(UiState.Success(Data())) State Flow stateFlow .onCompletion { println("ON

    COMPLETE") } .collect { println(it) }
  95. val stateFlow = MutableStateFlow(UiState.Success(Data())) State Flow stateFlow .onCompletion { println("ON

    COMPLETE") } .collect { println(it) } Will not log
  96. @Test fun `should emit default value`() = runBlockingTest { stateFlow

    .onCompletion { println("ON COMPLETE") } .test { expectItem() shouldBe UIState.Success } } State Flow
  97. @Test fun `should emit default value`() = runBlockingTest { stateFlow

    .onCompletion { println("ON COMPLETE") } .test { expectItem() shouldBe UIState.Success } } State Flow Will log
  98. How Turbine Works suspend fun <T> Flow<T>.test( ... ) {

    coroutineScope { 
 val collectJob = launch { collect { item -> ... } ... } ... collectJob.cancel() }
  99. @Test fun `should emit default value`() = runBlockingTest { stateFlow

    .onCompletion { println("ON COMPLETE") } .test { expectItem() shouldBe UIState.Success } } State Flow Will log
  100. State Flow Features • Conflate Flow Emissions • Get most

    recent emitted item
  101. val stateFlow = MutableStateFlow<UIState>(UIState.Success) @Test fun `should emit default value`()

    = runBlockingTest { stateFlow.emit(UIState.Error) stateFlow.test { expectItem() shouldBe UIState.Success expectItem() shouldBe UIState.Error } } State Flow
  102. val stateFlow = MutableStateFlow<UIState>(UIState.Success) @Test fun `should emit default value`()

    = runBlockingTest { stateFlow.emit(UIState.Error) stateFlow.test { expectItem() shouldBe UIState.Success expectItem() shouldBe UIState.Error } } State Flow
  103. val stateFlow = MutableStateFlow<UIState>(UIState.Success) @Test fun `should emit default value`()

    = runBlockingTest { stateFlow.emit(UIState.Error) stateFlow.test { expectItem() shouldBe UIState.Success expectItem() shouldBe UIState.Error } } State Flow Fail
  104. val stateFlow = MutableStateFlow<UIState>(UIState.Success) @Test fun `should emit default value`()

    = runBlockingTest { stateFlow.emit(UIState.Error) stateFlow.test { expectItem() shouldBe UIState.Success expectItem() shouldBe UIState.Error } } State Flow Most Recent Emission
  105. val stateFlow = MutableStateFlow<UIState>(UIState.Success) @Test fun `should emit default value`()

    = runBlockingTest { stateFlow.emit(UIState.Error) stateFlow.test { expectItem() shouldBe UIState.Error } } State Flow Success
  106. State Flow Features • Conflate Flow Emissions • Get most

    recent emitted item
  107. Mocking Flows View View Model Repository

  108. Mocking Flows View Model Repository

  109. Mocking Flows View Model Repository How to create a fake?

  110. Mocking Flows Approaches • Channels • State/Shared Flows

  111. Mocking Flows interface Repository { fun getData(): Flow<Result> }

  112. Mocking Flows class FakeRepository: Repository { private val channel =

    Channel<Result>() 
 }
  113. Mocking Flows class FakeRepository: Repository { private val channel =

    Channel<Result>() 
 override fun getData(): Flow<Result> { return channel.consumeAsFlow() } }
  114. Mocking Flows class FakeRepository: Repository { private val channel =

    Channel<Result>() 
 suspend fun emitResult(result: Result) { channel.send(result) } }
  115. Mocking Flows class FakeRepository: Repository { private val channel =

    Channel<Result>() 
 suspend fun emitFailure(result: Result.Failure) { channel.send(result) } }
  116. Mocking Flows class FakeRepository: Repository { private val channel =

    Channel<Result>() 
 fun closeChannel() = apply { channel.close() } }
  117. Mocking Flows Approaches • Channels • State/Shared Flows

  118. Mocking Flows class FakeRepository: Repository { val _stateFlow = MutableStateFlow<Result>(Result.Success)

    
 }
  119. Mocking Flows class FakeRepository: Repository { val _stateFlow = MutableStateFlow<Result>(Result.Success)

    
 val stateFlow = _stateFlow.asStateFlow() }
  120. Mocking Flows class FakeRepository: Repository { val _stateFlow = MutableStateFlow<Result>(Result.Success)

    
 fun emitResult(result: Result) { _stateFlow.value = result } }
  121. State Flow Considerations • Hard to test back to back

    emissions • Conflates emissions
  122. Better Approach • Use Shared Flow • Test the effects

    of each emission
  123. State Flow View View Model State

  124. State Flow State Flow Flow 1 Flow 2 Flow 3

    Combined Flow
  125. Flow Combine 1 A 1A Flow 1 Flow 2 Combined

  126. Flow Combine 1 2 A 1A 2A Flow 1 Flow

    2 Combined
  127. Flow Combine 1 2 A 1A 2A Flow 1 Flow

    2 Combined B 2B
  128. State Flow View View Model State Holder

  129. val stateFlow = MutableStateFlow<UIState>(UIState.Success) 
 flow1.combine(flow2) { a, b ->

    } State Flow
  130. val stateFlow = MutableStateFlow<UIState>(UIState.Success) 
 flow1.combine(flow2) { a, b ->

    combineItems(a, b) } State Flow
  131. val stateFlow = MutableStateFlow<UIState>(UIState.Success) 
 flow1.combine(flow2) { a, b ->

    combineItems(a, b) }.collect { stateFlow.emit(it) } State Flow
  132. @Test fun `should combine flows`() = runBlockingTest { 
 val

    mockFlow1 = mockFlow() 
 val mockFlow2 = mockFlow() } State Flow
  133. @Test fun `should combine flows`() = runBlockingTest { 
 val

    mockFlow1 = mockFlow() 
 val mockFlow2 = mockFlow() 
 val stateHolder = StateHolder(mockFlow1, mockFlow2) } State Flow
  134. @Test fun `should combine flows`() = runBlockingTest { 
 


    val stateHolder = StateHolder(mockFlow1, mockFlow2) 
 stateFlow.test { 
 ... 
 } } State Flow
  135. State Flow Challenges • Why didn’t the flow emit an

    item? Conflated? • Too many flow combines makes it harder to follow the 
 
 data flow
  136. Testing Shared Flows Shared Flow Use Cases

  137. Shared Flow

  138. Shared Flow Consumer 1 Consumer 2

  139. Shared Flow Consumer 1 Consumer 2

  140. Shared Flow Consumer 1 Consumer 2 Event Event

  141. Shared Flow Consumer 1 Consumer 2 Replay Replay

  142. 
 Shared Flow Buffer

  143. Shared Flow val flow = MutableSharedFlow <> ()

  144. Shared Flow val flow = MutableSharedFlow<String>()

  145. Shared Flow val flow = MutableSharedFlow<String>() @Test fun `collect from

    shared flow`() = runBlockingTest { flow.emit("Event 1") }
  146. Shared Flow val flow = MutableSharedFlow<String>() @Test fun `collect from

    shared flow`() = runBlockingTest { flow.emit("Event 1") flow.test { expectItem() shouldBeEqualTo "Event 1" } } Failed
  147. Shared Flow val flow = MutableSharedFlow<String>() @Test fun `collect from

    shared flow`() = runBlockingTest { flow.emit("Event 1") flow.test { expectItem() shouldBeEqualTo "Event 1" } } Subscriber count 0
  148. Shared Flow val flow = MutableSharedFlow<String>() @Test fun `collect from

    shared flow`() = runBlockingTest { flow.emit("Event 1") flow.test { expectItem() shouldBeEqualTo "Event 1" } } Flow doesn’t replay
  149. Shared Flow @Test fun `collect from shared flow`() = runBlockingTest

    { 
 val job = launch(start = CoroutineStart.LAZY) { flow.emit("Event 1") } }
  150. Shared Flow @Test fun `collect from shared flow`() = runBlockingTest

    { 
 val job = launch(start = CoroutineStart.LAZY) { flow.emit("Event 1") } }
  151. Shared Flow @Test fun `collect from shared flow`() = runBlockingTest

    { 
 val job = launch(start = CoroutineStart.LAZY) { flow.emit("Event 1") } flow.test { job.start() expectItem() shouldBeEqualTo "Event 1" } } Subscriber count 1
  152. Shared Flow @Test fun `collect from shared flow`() = runBlockingTest

    { 
 val job = launch(start = CoroutineStart.LAZY) { flow.emit("Event 1") } flow.test { job.start() expectItem() shouldBeEqualTo "Event 1" } } Success
  153. Shared Flow Features • Replay

  154. Shared Flow val flow = MutableSharedFlow<String>(replay = 1)

  155. @Test fun `collect from shared flow`() = runBlockingTest { flow.emit("Event

    1") flow.test { expectItem() shouldBeEqualTo "Event 1" } } Shared Flow val flow = MutableSharedFlow<String>(replay = 1) Replay
  156. Mocking Flows View Model Repository How to create a fake?

  157. Mocking Flows Approaches • Channels • State/Shared Flows

  158. Mocking Flows class FakeRepository: Repository { val flow = MutableSharedFlow<Result>(

    
 replay = 1, 
 BufferOverFlow.DROP_OLDEST 
 ) 
 }
  159. Mocking Flows class FakeRepository: Repository { val flow = MutableSharedFlow<Result>(

    
 replay = 1, 
 BufferOverFlow.DROP_OLDEST 
 ) 
 suspend fun emitResult(result: Result) { flow.emit(result) } }
  160. Shared Flow Approach • No conflation • Handle testing each

    emission
  161. Converting to Shared Flows • While Subscribed • Eagerly •

    Lazily
  162. While Subscribed val flow = upstreamFlow()

  163. flow.shareIn( sharingScope, SharingStarted.WhileSubscribed(), replay = 1 ) While Subscribed

  164. flow.shareIn( sharingScope, SharingStarted.WhileSubscribed(), replay = 1 ) While Subscribed

  165. flow.shareIn( sharingScope, SharingStarted.WhileSubscribed(), replay = 1 ) While Subscribed

  166. flow.shareIn( sharingScope, SharingStarted.WhileSubscribed(), replay = 1 ) While Subscribed

  167. While Subscribed @Test fun `collect with while subscribed strategy`() =

    runBlockingTest { 
 val sharingScope = TestCoroutineScope() val sharedFlow = flow .onStart { println("ON START") } .shareIn( sharingScope, SharingStarted.WhileSubscribed(), 1 ) }
  168. While Subscribed @Test fun `collect with while subscribed strategy`() =

    runBlockingTest { 
 val sharingScope = TestCoroutineScope() val sharedFlow = flow .shareIn( sharingScope, SharingStarted.WhileSubscribed(), 1 ) }
  169. While Subscribed @Test fun `collect with while subscribed strategy`() =

    runBlockingTest { 
 val sharingScope = TestCoroutineScope() val sharedFlow = flow .onStart { println("ON START") } .shareIn( sharingScope, SharingStarted.WhileSubscribed(), 1 ) } Will not log
  170. While Subscribed • Start upstream flow upon subscriber • Remain

    active while external scope is active
  171. While Subscribed @Test fun `collect with while subscribed strategy`() =

    runBlockingTest { 
 val sharedFlow = flow .onStart { println("ON START") } .shareIn( ... ) sharedFlow.test { ... } }
  172. While Subscribed @Test fun `collect with while subscribed strategy`() =

    runBlockingTest { 
 val sharedFlow = flow .onStart { println("ON START") } .shareIn( ... ) sharedFlow.test { ... } }
  173. While Subscribed • Start upstream flow upon subscriber • Remain

    active while there are subscribers
  174. Eagerly • Start upstream flow upon creation

  175. Eagerly @Test fun `collect with eagerly strategy`() = runBlockingTest {

    
 val sharedFlow = flow .onStart { println("ON START") } .shareIn( sharingScope, SharingStarted.Eagerly, 1 ) }
  176. Eagerly @Test fun `collect with eagerly strategy`() = runBlockingTest {

    
 val sharedFlow = flow .onStart { println("ON START") } .shareIn( sharingScope, SharingStarted.Eagerly, 1 ) } Will start
  177. Lazily • Start upstream flow upon subscription • Never stops

    upstream
  178. Lazily @Test fun `collect with lazily strategy`() = runBlockingTest {

    
 val sharedFlow = flow .onStart { println("ON START") } .shareIn( sharingScope, SharingStarted.Lazily, 1 ) } Will start
  179. Lazily @Test fun `collect with lazily strategy`() = runBlockingTest {

    
 val sharedFlow = flow .onComplete { println("ON Complete”) } .shareIn( sharingScope, SharingStarted.Lazily, 1 ) }
  180. Lazily • Start upstream flow upon subscription • Never stops

    upstream
  181. Converting to Shared Flows • While Subscribed • Eagerly •

    Lazily
  182. https: // codingwithmohit.com/coroutines/learning-shared-and-state-flows-with-tests/ Coding with Mohit

  183. Thank You! www.codingwithmohit.com @heyitsmohit