GithubHelp home page GithubHelp logo

cashapp / turbine Goto Github PK

View Code? Open in Web Editor NEW
2.3K 24.0 99.0 1.67 MB

A small testing library for kotlinx.coroutines Flow

Home Page: https://cashapp.github.io/turbine/docs/1.x/

License: Apache License 2.0

Kotlin 100.00%

turbine's Introduction

Turbine

Turbine is a small testing library for kotlinx.coroutines Flow.

flowOf("one", "two").test {
  assertEquals("one", awaitItem())
  assertEquals("two", awaitItem())
  awaitComplete()
}

A turbine is a rotary mechanical device that extracts energy from a fluid flow and converts it into useful work.

โ€“ Wikipedia

Download

repositories {
  mavenCentral()
}
dependencies {
  testImplementation("app.cash.turbine:turbine:1.1.0")
}
Snapshots of the development version are available in Sonatype's snapshots repository.

repositories {
  maven {
    url = uri("https://oss.sonatype.org/content/repositories/snapshots/")
  }
}
dependencies {
  testImplementation("app.cash.turbine:turbine:1.2.0-SNAPSHOT")
}

While Turbine's own API is stable, we are currently forced to depend on an unstable API from kotlinx.coroutines test artifact: UnconfinedTestDispatcher. Without this usage of Turbine with runTest would break. It's possible for future coroutine library updates to alter the behavior of this library as a result. We will make every effort to ensure behavioral stability as well until this API dependency is stabilized (tracking issue #132).

Usage

A Turbine is a thin wrapper over a Channel with an API designed for testing.

You can call awaitItem() to suspend and wait for an item to be sent to the Turbine:

assertEquals("one", turbine.awaitItem())

...awaitComplete() to suspend until the Turbine completes without an exception:

turbine.awaitComplete()

...or awaitError() to suspend until the Turbine completes with a Throwable.

assertEquals("broken!", turbine.awaitError().message)

If await* is called and nothing happens, Turbine will timeout and fail instead of hanging.

When you are done with a Turbine, you can clean up by calling cancel() to terminate any backing coroutines. Finally, you can assert that all events were consumed by calling ensureAllEventsConsumed().

Single Flow

The simplest way to create and run a Turbine is produce one from a Flow. To test a single Flow, call the test extension:

someFlow.test {
  // Validation code here!
}

test launches a new coroutine, calls someFlow.collect, and feeds the results into a Turbine. Then it calls the validation block, passing in the read-only ReceiveTurbine interface as a receiver:

flowOf("one").test {
  assertEquals("one", awaitItem())
  awaitComplete()
}

When the validation block is complete, test cancels the coroutine and calls ensureAllEventsConsumed().

Multiple Flows

To test multiple flows, assign each Turbine to a separate val by calling testIn instead:

runTest {
  turbineScope {
    val turbine1 = flowOf(1).testIn(backgroundScope)
    val turbine2 = flowOf(2).testIn(backgroundScope)
    assertEquals(1, turbine1.awaitItem())
    assertEquals(2, turbine2.awaitItem())
    turbine1.awaitComplete()
    turbine2.awaitComplete()
  }
}

Like test, testIn produces a ReceiveTurbine. ensureAllEventsConsumed() will be invoked when the calling coroutine completes.

testIn cannot automatically clean up its coroutine, so it is up to you to ensure that the running flow terminates. Use runTest's backgroundScope, and it will take care of this automatically. Otherwise, make sure to call one of the following methods before the end of your scope:

  • cancel()
  • awaitComplete()
  • awaitError()

Otherwise, your test will hang.

Consuming All Events

Failing to consume all events before the end of a flow-based Turbine's validation block will fail your test:

flowOf("one", "two").test {
  assertEquals("one", awaitItem())
}
Exception in thread "main" AssertionError:
  Unconsumed events found:
   - Item(two)
   - Complete

The same goes for testIn, but at the end of the calling coroutine:

runTest {
  turbineScope {
    val turbine = flowOf("one", "two").testIn(backgroundScope)
    turbine.assertEquals("one", awaitItem())
  }
}
Exception in thread "main" AssertionError:
  Unconsumed events found:
   - Item(two)
   - Complete

Received events can be explicitly ignored, however.

flowOf("one", "two").test {
  assertEquals("one", awaitItem())
  cancelAndIgnoreRemainingEvents()
}

Additionally, we can receive the most recent emitted item and ignore the previous ones.

flowOf("one", "two", "three")
  .map {
    delay(100)
    it
  }
  .test {
    // 0 - 100ms -> no emission yet
    // 100ms - 200ms -> "one" is emitted
    // 200ms - 300ms -> "two" is emitted
    // 300ms - 400ms -> "three" is emitted
    delay(250)
    assertEquals("two", expectMostRecentItem())
    cancelAndIgnoreRemainingEvents()
  }

Flow Termination

Flow termination events (exceptions and completion) are exposed as events which must be consumed for validation. So, for example, throwing a RuntimeException inside of your flow will not throw an exception in your test. It will instead produce a Turbine error event:

flow { throw RuntimeException("broken!") }.test {
  assertEquals("broken!", awaitError().message)
}

Failure to consume an error will result in the same unconsumed event exception as above, but with the exception added as the cause so that the full stacktrace is available.

flow<Nothing> { throw RuntimeException("broken!") }.test { }
app.cash.turbine.TurbineAssertionError: Unconsumed events found:
 - Error(RuntimeException)
	at app//app.cash.turbine.ChannelTurbine.ensureAllEventsConsumed(Turbine.kt:215)
  ... 80 more
Caused by: java.lang.RuntimeException: broken!
	at example.MainKt$main$1.invokeSuspend(FlowTest.kt:652)
	... 105 more

Standalone Turbines

In addition to ReceiveTurbines created from flows, standalone Turbines can be used to communicate with test code outside of a flow. Use them everywhere, and you might never need runCurrent() again. Here's an example of how to use Turbine() in a fake:

class FakeNavigator : Navigator {
  val goTos = Turbine<Screen>()

  override fun goTo(screen: Screen) {
    goTos.add(screen)
  }
}
runTest {
  val navigator = FakeNavigator()
  val events: Flow<UiEvent> =
    MutableSharedFlow<UiEvent>(extraBufferCapacity = 50)
  val models: Flow<UiModel> =
    makePresenter(navigator).present(events)
  models.test {
    assertEquals(UiModel(title = "Hi there"), awaitItem())
    events.emit(UiEvent.Close)
    assertEquals(Screens.Back, navigator.goTos.awaitItem())
  }
}

Standalone Turbine Compat APIs

To support codebases with a mix of coroutines and non-coroutines code, standalone Turbine includes non-suspending compat APIs. All the await methods have equivalent take methods that are non-suspending:

val navigator = FakeNavigator()
val events: PublishRelay<UiEvent> = PublishRelay.create()

val models: Observable<UiModel> =
  makePresenter(navigator).present(events)
val testObserver = models.test()
testObserver.assertValue(UiModel(title = "Hi there"))
events.accept(UiEvent.Close)
assertEquals(Screens.Back, navigator.goTos.takeItem())

Use takeItem() and friends, and Turbine behaves like simple queue; use awaitItem() and friends, and it's a Turbine.

These methods should only be used from a non-suspending context. On JVM platforms, they will throw when used from a suspending context.

Asynchronicity and Turbine

Flows are asynchronous by default. Your flow is collected concurrently by Turbine alongside your test code.

Handling this asynchronicity works the same way with Turbine as it does in production coroutines code: instead of using tools like runCurrent() to "push" an asynchronous flow along, Turbine's awaitItem(), awaitComplete(), and awaitError() "pull" them along by parking until a new event is ready.

channelFlow {
  withContext(IO) {
    Thread.sleep(100)
    send("item")
  }
}.test {
  assertEquals("item", awaitItem())
  awaitComplete()
}

Your validation code may run concurrently with the flow under test, but Turbine puts it in the driver's seat as much as possible: test will end when your validation block is done executing, implicitly cancelling the flow under test.

channelFlow {
  withContext(IO) {
    repeat(10) {
      Thread.sleep(200)
      send("item $it")
    }
  }
}.test {
  assertEquals("item 0", awaitItem())
  assertEquals("item 1", awaitItem())
  assertEquals("item 2", awaitItem())
}

Flows can also be explicitly canceled at any point.

channelFlow {
  withContext(IO) {
    repeat(10) {
      Thread.sleep(200)
      send("item $it")
    }
  }
}.test {
  Thread.sleep(700)
  cancel()

  assertEquals("item 0", awaitItem())
  assertEquals("item 1", awaitItem())
  assertEquals("item 2", awaitItem())
}

Names

Turbines can be named to improve error feedback. Pass in a name to test, testIn, or Turbine(), and it will be included in any errors that are thrown:

runTest {
  turbineScope {
    val turbine1 = flowOf(1).testIn(backgroundScope, name = "turbine 1")
    val turbine2 = flowOf(2).testIn(backgroundScope, name = "turbine 2")
    turbine1.awaitComplete()
    turbine2.awaitComplete()
  }
}
Expected complete for turbine 1 but found Item(1)
app.cash.turbine.TurbineAssertionError: Expected complete for turbine 1 but found Item(1)
	at app//app.cash.turbine.ChannelKt.unexpectedEvent(channel.kt:258)
	at app//app.cash.turbine.ChannelKt.awaitComplete(channel.kt:226)
	at app//app.cash.turbine.ChannelKt$awaitComplete$1.invokeSuspend(channel.kt)
	at app//kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	...

Order of Execution & Shared Flows

Shared flows are sensitive to order of execution. Calling emit before calling collect will drop the emitted value:

val mutableSharedFlow = MutableSharedFlow<Int>(replay = 0)
mutableSharedFlow.emit(1)
mutableSharedFlow.test {
  assertEquals(awaitItem(), 1)
}
No value produced in 1s
java.lang.AssertionError: No value produced in 1s
	at app.cash.turbine.ChannelKt.awaitEvent(channel.kt:90)
	at app.cash.turbine.ChannelKt$awaitEvent$1.invokeSuspend(channel.kt)
	(Coroutine boundary)
	at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTestCoroutine$2.invokeSuspend(TestBuilders.kt:212)

Turbine's test and testIn methods guarantee that the flow under test will run up to the first suspension point before proceeding. So calling test on a shared flow before emitting will not drop:

val mutableSharedFlow = MutableSharedFlow<Int>(replay = 0)
mutableSharedFlow.test {
  mutableSharedFlow.emit(1)
  assertEquals(awaitItem(), 1)
}

If your code collects on shared flows, ensure that it does so promptly to have a lovely experience.

The shared flow types Kotlin currently provides are:

  • MutableStateFlow
  • StateFlow
  • MutableSharedFlow
  • SharedFlow

Timeouts

Turbine applies a timeout whenever it waits for an event. This is a wall clock time timeout that ignores runTest's virtual clock time.

The default timeout length is one second. This can be overridden by passing a timeout duration to test:

flowOf("one", "two").test(timeout = 10.milliseconds) {
  ...
}

This timeout will be used for all Turbine-related calls inside the validation block.

You can also override the timeout for Turbines created with testIn and Turbine():

val standalone = Turbine<String>(timeout = 10.milliseconds)
val flow = flowOf("one").testIn(
  scope = backgroundScope,
  timeout = 10.milliseconds,
)

These timeout overrides only apply to the Turbine on which they were applied.

Finally, you can also change the timeout for a whole block of code using withTurbineTimeout:

withTurbineTimeout(10.milliseconds) {
  ...
}

Channel Extensions

Most of Turbine's APIs are implemented as extensions on Channel. The more limited API surface of Turbine is usually preferable, but these extensions are also available as public APIs if you need them.

License

Copyright 2018 Square, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

turbine's People

Contributors

abendt avatar antimonit avatar autonomousapps avatar burntcookie90 avatar dependabot[bot] avatar filograno avatar goooler avatar hfhbd avatar hishammuneer avatar hoc081098 avatar jakewharton avatar jingibus avatar johannesptaszyk avatar johnjohndoe avatar jrodbx avatar kpgalligan avatar kubode avatar mars885 avatar ntsk avatar paulwoitaschek avatar renovate[bot] avatar rupinderjeet avatar russhwolf avatar sebastianaigner avatar shivampokhriyal avatar starsep avatar stefma avatar trevjonez avatar ychescale9 avatar yshrsmz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

turbine's Issues

Getting UncompletedCoroutinesError when emit value twice in viewModelScope

Hey.

When I emit value twice in viewModelScope, I can only get one value using awaitItem().


// in ViewModel
private val _showProgressIndicator = MutableStateFlow(false)
val showProgressIndicator = _showProgressIndicator.asStateFlow()

fun doingSomeSuspendThing() {
viewModelScope.launch {
   // show progress indicator
    _showProgressIndicator.emit(true)
    // do something we need

   // hide the progress indicator
   _showProgressIndicator.emit(false)
}
}

//Test Class
@test
fun test() = runTest {
viewModel.showProgressIndicator.test {
// mock
// when
viewModel.doingSomeSuspendThing()
// then
assertEquals(false, awaitEvent())
assertEquals(true, awaitEvent())
// If I ignore this line, it will be passed the test.
assertEquals(false, awaitEvent())
}
}

Could you give me some suggestions? ๐Ÿ™

Test block is not receiving all emitted values of the StateFlow

Hi,
I am quite new to coroutines and StateFlow so not sure If im doing something wrong.
Essentially I have a StateFlow which described the state of my screen using a Sealed Class.
This sealed class gets updated values, like so:

intent.consumeAsFlow().collect {
    when (it) {
                is LoginActions.LoginButtonClicked -> {
                    _state.value = LoginScreenState.Loading(true)
                    val result = loginWithEmailAndPasswordUseCase(it.email, it.password)
                    _state.value = LoginScreenState.Loading(false)
                    _state.value = when (result) {
                        is LoginResult.Success -> {
                            when {
                                result.isEmailVerified -> LoginScreenState.Success
                                result.isEmailVerified.not() -> LoginScreenState.EmailNeedsToBeVerified
                                else -> LoginScreenState.Failure(listOf(LoginError.Unknown))
                            }
                        }
                        is LoginResult.Failure -> LoginScreenState.Failure(result.errors)
                    }
                }
            }
        }

In my UI I do get loading=true and then loading=false and then success states.
When writing my test using turbine I have it like this:

@Test
    fun `given email has been verified then login successfully`() = testDispatcher.runBlockingTest {
        whenever(loginWithEmailVerificationUseCase(anyString(), anyString()))
            .thenReturn(LoginResult.Success("[email protected]", isEmailVerified = true))

        vm.intent.send(LoginActions.LoginButtonClicked("[email protected]", "email123"))
        vm.state.test {
            assertEquals(LoginScreenState.Loading(true), expectItem())
            assertEquals(LoginScreenState.Loading(false), expectItem())
            assertEquals(LoginScreenState.Success, expectItem())
            expectComplete()
        }
    }

Although on the test block I am only getting the Success state back and so it fails the test.
Is there something that I am doing wrong? Or is this a bug with the lib?

SharedFlow hangs when using SharingStarted.WhileSubscribed

Related to #33. The following test doesn't complete until timeout is reached:

@Test
fun failingTurbineTest() = runTest {
    val sharedFlow = (1..3).asFlow()
    .shareIn(this, SharingStarted.WhileSubscribed())

    sharedFlow.test {
        assertThat(awaitItem()).isEqualTo(1)
        assertThat(awaitItem()).isEqualTo(2)
        assertThat(awaitItem()).isEqualTo(3)
        cancelAndIgnoreRemainingEvents()
    }
}

However, if I instead use SharingStarted.Eagerly or SharingStarted.Lazily the test completes fine. Also note I'm using runTest.

Cancel and return unconsumed events method

I'm writing a testing DSL that sits atop Turbine. This DSL automatically cancels and wants to fail if you forget to consume some events. The problem is that the error message from Turbine is not great here.

This could either be a way to peek at the unconsumed event list or a cancel overload that returns the list of unconsumed events.

Issue with `awaitItem()`, `StateFlow` and 0.9.0 version

I'm facing some issues when using StateFlow and upgrading to latest 0.9.0 version. Only the first and last items are received by awaitItem().
Same tests pass on versions prior 0.9.0. Tried 0.8.0 and 0.7.0.

class FlowTest {

    private val mutableStateFlow = MutableStateFlow(0)
    val stateFlow = mutableStateFlow.asStateFlow()

    fun updateValues() {
        mutableStateFlow.update { 1 }
        mutableStateFlow.update { 2 }
        mutableStateFlow.update { 3 }
        mutableStateFlow.update { 4 }
        mutableStateFlow.update { 5 }
    }

    fun setValues() {
        mutableStateFlow.value = 1
        mutableStateFlow.value = 2
        mutableStateFlow.value = 3
        mutableStateFlow.value = 4
        mutableStateFlow.value = 5
    }
}

@ExperimentalCoroutinesApi
class FlowTestTest {

    @Test
    fun updateValues() = runTest {
        val sut = FlowTest()

        sut.stateFlow.test {
            sut.updateValues()

            assertEquals(0, awaitItem())
            assertEquals(1, awaitItem())
            assertEquals(2, awaitItem())
            assertEquals(3, awaitItem())
            assertEquals(4, awaitItem())
            assertEquals(5, awaitItem())
            assert(cancelAndConsumeRemainingEvents().isEmpty())
        }
    }

    @Test
    fun setValues() = runTest {
        val sut = FlowTest()

        sut.stateFlow.test {
            sut.setValues()

            assertEquals(0, awaitItem())
            assertEquals(1, awaitItem())
            assertEquals(2, awaitItem())
            assertEquals(3, awaitItem())
            assertEquals(4, awaitItem())
            assertEquals(5, awaitItem())
            assert(cancelAndConsumeRemainingEvents().isEmpty())
        }
    }

    @Test
    fun flowOfValues() = runTest {
        flowOf(0, 1, 2, 3, 4, 5).test {
            assertEquals(0, awaitItem())
            assertEquals(1, awaitItem())
            assertEquals(2, awaitItem())
            assertEquals(3, awaitItem())
            assertEquals(4, awaitItem())
            assertEquals(5, awaitItem())
            awaitComplete()
        }
    }
}

Test Results

Turbine 0.9.0

image

image

image

Turbine 0.8.0, 0.7.0

image

Versions

testImplementation 'junit:junit:4.13.2'
testImplementation 'app.cash.turbine:turbine:0.9.0'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'

When use liveData and change to flow by .asFlow() test won't trigger events

Hi
I have an issue when convert live data to flow like:

 fun foo(
    ): Flow<B> =
        bar().asFlow()
            .map { -> }
foo().test{
 val result = awaitItem()
 Assert.assertEquals(expected, result)
 awaitComplete()
}

Then I got this error
After waiting for 60000 ms, the test coroutine is not completing, there were active child jobs: [ScopeCoroutine{Active}@35c3ef4a]
Previously I faced this issue and find out that in some case should awaitComplete() but it some case that will also not works.

GH releases do not have binaries attached

github-release is a separate job from the publish job. When binaries are collected by action-gh-release step they are not present in the directory.

- name: Create release
uses: softprops/action-gh-release@v1
with:
body: ${{ steps.release_notes.outputs.release_notes }}
files: build/libs/*

https://github.com/cashapp/turbine/runs/1024853027?check_suite_focus=true#step:5:10

Since github-release job needs publish anyway they could be merged into a one job. This is just a suggestion as I don't know if there is a better way to do this.

On the side note, it would be nice to add binaries to already published GH releases retroactively.

test(timeout) doesn't seem to work

See the screenshot: the tests claim that they timed out after waiting for 1000ms, but none of the test methods run for that long, most of them failing after ~20ms.

image

Turbine skips item emissions

When using coroutines 1.6.1, Kotlin 1.6.21 and turbine 0.8.0, turbine skips emissions of items(issue was reported here #91 but for turbine version 0.7.0)

private val stateFlow = MutableStateFlow(0)
private val flow: Flow<Int> = stateFlow

@Test
fun `turbine test`() = runTest {
    Dispatchers.setMain(UnconfinedTestDispatcher())

    val scope = CoroutineScope(Job() + Dispatchers.Main)

    flow.test {
        flowOf(1, 2, 3)
            .onEach { stateFlow.value = it }
            .launchIn(scope)

        val events = cancelAndConsumeRemainingEvents()
    }

    Dispatchers.resetMain()
}

events will contain only the first and last items: [Item(0), Item(3)] instead of [Item(0), Item(1), Item(2), Item(3)]

Updating to 0.5.2 cause :shared:linkDebugTestIosX64 fail

Hey everyone, I wanted to share this issue I ran into since updating turbine to 0.5.2 from 0.4.1.
I'm on a Kotlin Multiplatform Project and using turbine to unit test on commonTest, androidTest and iosTest source sets.
In order to get this #46 fix I updated to 0.5.2.
This release now supports kotlin native builds but it seems to introduce the following issue when building ios tests:

e: Compilation failed: Deserializer for declaration public kotlinx.coroutines.channels/ValueOrClosed|null[0] is not found
 * Source files: 
 * Compiler version info: Konan: 1.5.10 / Kotlin: 1.5.10
 * Output kind: STATIC_CACHE
e: java.lang.IllegalStateException: Deserializer for declaration public kotlinx.coroutines.channels/ValueOrClosed|null[0] is not found
	at org.jetbrains.kotlin.backend.common.serialization.KotlinIrLinker.handleNoModuleDeserializerFound(KotlinIrLinker.kt:473)
	at org.jetbrains.kotlin.backend.common.serialization.KotlinIrLinker$IrDeserializerForFile.findModuleDeserializer(KotlinIrLinker.kt:361)
	at org.jetbrains.kotlin.backend.common.serialization.KotlinIrLinker$IrDeserializerForFile.deserializeIrSymbolData(KotlinIrLinker.kt:390)
	at org.jetbrains.kotlin.backend.common.serialization.KotlinIrLinker$IrDeserializerForFile.deserializeIrSymbol(KotlinIrLinker.kt:406)
	at org.jetbrains.kotlin.backend.common.serialization.IrFileDeserializer.deserializeIrSymbolAndRemap(IrFileDeserializer.kt:145)
	at org.jetbrains.kotlin.backend.common.serialization.IrFileDeserializer.deserializeSimpleType(IrFileDeserializer.kt:170)
	at org.jetbrains.kotlin.backend.common.serialization.IrFileDeserializer.deserializeIrTypeData(IrFileDeserializer.kt:214)
	at ....

As per this https://youtrack.jetbrains.com/issue/KT-41378 It seems being related to coroutines dependency being overridden.
I see Turbine depends on 1.5.0 coroutine and not on 1.5.0-native-mt so on my side I did provide the strictly("1.5.0-native-mt") clause but still no improvements.

Do you see any reason why It won't override Turbine's dependency?

Is this the intended use with StateFlow?

Recently I used turbine to test some StateFlow's and came across a particular interaction and I'm mostly wondering what is the intended use. Both of the below cases fail.

oven.preWarm(300)
oven.states.test { state ->
  assertEquals(WARMING(100), expectItem())
  assertEquals(WARMING(200), expectItem())
  assertEquals(PREWARM_COMPLETE, expectItem())

  cancelAndIgnoreRemainingEvents()
}

For this case calling expectItem() only receives the final state expected (PREWARM_COMPLETE) from the preWarm() call and will fail the assertions. This makes sense as the collect operator is starting when our StateFlow has already emitted up to this state.

oven.states.test { state ->
  assertEquals(WARMING(100), expectItem())
  assertEquals(WARMING(200), expectItem())
  assertEquals(PREWARM_COMPLETE, expectItem())

  cancelAndIgnoreRemainingEvents()
}

oven.preWarm(300)

For this case there is an exception for a default timeout:
kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms

or for timeout = Duration.ZERO
java.lang.IllegalStateException: This job has not completed yet


However, when used like the below, moving the call to trigger state emission to the validate lambda, all expected events are received and the test passes. This is because validate is called after collection starts on the receiver flow.

oven.states.test { state ->
  oven.preWarm(300)
  assertEquals(WARMING(100), expectItem())
  assertEquals(WARMING(200), expectItem())
  assertEquals(PREWARM_COMPLETE, expectItem())

  cancelAndIgnoreRemainingEvents()
}

And also launching in a new job and calling test and the emission after succeeds as well.

launch {
  oven.states.test { state ->
    assertEquals(WARMING(100), expectItem())
    assertEquals(WARMING(200), expectItem())
    assertEquals(PREWARM_COMPLETE, expectItem())

    cancelAndIgnoreRemainingEvents()
  }

  oven.preWarm(300)
}

The documentation provided so far doesn't provide much information for a best practice on how to handle Flows that have their emissions triggered elsewhere. What is the best practice here? I would assume that launching a new job should not be required.


Note: I am using this rule to runBlockingTests

@ExperimentalCoroutinesApi
class MainCoroutineRule(
    val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher(),
) : TestWatcher() {

    override fun starting(description: Description?) {
        super.starting(description)
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description?) {
        super.finished(description)
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }

    /**
     * A convenience function to prevent the need to call a long chain for the provided dispatcher
     */
    fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) {
        testDispatcher.runBlockingTest { block() }
    }
}

How can I test MutableSharedFlow<Unit>() with turbine ?

Hi,
when i press button MutableSharedFlow() is emiting. So how can i test this with turbine emiting or not ?

// in ViewModel
private val _updateAddress = MutableSharedFlow()
val updateAddress = _updateAddress.asSharedFlow()

fun updateAddressClick() {
viewModelScope.launch {
_updateAddress.emit(Unit)
}
}

//Test Class
@test
fun updateAddressBtn_clickEvent_Emiting_or_Not() = runTest {
viewModel.updateAddress.test {
// given
// when
viewModel.updateAddressClick()
// then
Truth.assertThat(awaitEvent())
}
}

expectNoEvents() not working?

I'm trying to test a Room Flow like the following:

 runBlocking {
    underTest.saveStuff(listOf(FIRST, SECOND)) // suspending DAO INSERT call

    roomExecutor.drain() // androidx.arch.core:core-testing's CountingTaskExecutorRule::drainTasks shortcut
    yield()

    underTest.observeStuff().test {
        assertThat(expectItem()).hasSize(2)
    }
}

Now when I execute this test, all is ok and green. But, to my surprise, if I verify like this

    underTest.observeStuff().test {
        expectNoEvents()
    }

the test won't fail either! I can even chain this and the test keeps green:

    underTest.observeStuff().test {
        expectNoEvents()
        assertThat(expectItem()).hasSize(2)
    }

How can it be that there are apparently no events verified, but if we look more closer, there are events? Can one rely at all on expectNoEvents()?

Add skip functionality

Sometimes it might be good to have a way to skip a certain number of events to have a cleaner test without multiple await calls, which are then not part of any assertion.
For my codebase I added an extension function like this:

suspend fun <T> FlowTurbine<T>.skipItem(amount: Int = 1) = repeat(amount) {
    awaitItem()
}

Does it make sense to add that functionality on library level?

assertValue/assertValues extension

This is a great library. Thanks a bunch for publishing it!

I like the syntax for RxJava tests which is something like this:

Observable.just(1, 2, 3).test().run {
    assertValues(1, 2, 3)
}

It's shorter/less verbose than:

flowOf(1, 2, 3).test {
    assertEquals(1, awaitItem())
    assertEquals(2, awaitItem())
    assertEquals(3, awaitItem())
    awaitComplete()
}

By adding these two small extension functions

suspend fun <T> FlowTurbine<T>.assertValue(expected: T) = coroutineScope {
    assertEquals(expected, awaitItem())
}

suspend fun <T> FlowTurbine<T>.assertValues(vararg expected: T) = coroutineScope {
   for (value in expected) assertValue(value)
}

You could write:

flowOf(1, 2, 3).test {
    assertValues(1, 2, 3)
    awaitComplete()
}

Would you consider adding these extension functions officially to the library?

Unexpected item received

Hi,

I am a bit lost with a test that keeps failing, no matter how I change it. I test a flow which should emit only models that are in an "existing" state:

testModel.existingFlow().onEach { println("Flow contains $it") }.test {
    for (modelState in allModelStates) {
        testModel.nextModelState = modelState
        testModel.listener.onModelChanged(testModel) // invoke flow emission
        if (modelState.exists()) {
            expectItem()
        } else {
            expectNoEvents() // AssertionError
        }
    }
    cancel()
}

The log prints like this:

Flow contains TestModel{state=SYNCHRONIZED, exists=true}
Flow contains TestModel{state=OUT_DATED, exists=true}
Flow contains TestModel{state=UPDATING, exists=true}
Flow contains TestModel{state=CREATING, exists=true}

app.cash.turbine.AssertionError: Expected no events but found Item(TestModel{state=DELETED, exists=false})

So, as you can see, the flow has emitted only the states that are considered as "existing", but somehow another item with state "DELETED" is received by the Turbine Flow.

By the way, thanks a lot for this great library! It makes testing flows so much more beautiful.

Turbine: 0.3.0
Kotlin: 1.4.21
Coroutines: 1.4.2

Coroutine dependency conflict when using "strictly" 1.4.2-native-mt

I'm trying to use Turbine in a project where I'm forcing native multithreading version of coroutines (like the docs for ktor suggest):

val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2-native-mt") {
                    version { strictly(1.4.2-native-mt) }
                }
                ...
            }
        }

        val commonTest by getting {
            dependencies {
                implementation("app.cash.turbine:turbine:0.4.1")
                ...
            }
        }

When running the test I get:

Initialization script '/private/var/folders/8n/โ€ฆ/T/ijresolvers.gradle' line: 119
* What went wrong:
Execution failed for task ':shared:generateDebugUnitTestStubRFile'.
> Could not resolve all files for configuration ':shared:debugUnitTestRuntimeClasspath'.
   > Could not resolve org.jetbrains.kotlinx:kotlinx-coroutines-core:{strictly 1.4.2-native-mt}.
     Required by:
         project :shared
      > Cannot find a version of 'org.jetbrains.kotlinx:kotlinx-coroutines-core' that satisfies the version constraints:
           Dependency path 'Project:shared:unspecified' --> 'Project:shared:unspecified' --> 'org.jetbrains.kotlinx:kotlinx-coroutines-core:{strictly 1.4.2-native-mt}'
           Dependency path 'Project:shared:unspecified' --> 'io.kotlintest:kotlintest-runner-junit5:3.3.2' --> 'io.kotlintest:kotlintest-core:3.3.2' --> 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
           Dependency path 'Project:shared:unspecified' --> 'io.kotlintest:kotlintest-runner-junit5:3.3.2' --> 'io.kotlintest:kotlintest-runner-jvm:3.3.2' --> 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
           Dependency path 'Project:shared:unspecified' --> 'app.cash.turbine:turbine:0.4.1' --> 'app.cash.turbine:turbine-jvm:0.4.1' --> 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3'
   > Could not resolve org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1.

It's probably resolvable through some clever gradle trickery, but I cannot quite work it out:

  • tried adding 1.4.3 as dependency for commonTest but effect is the same
  • tried using the same strictly 1.4.2-native-mt config in commonTest but I get this instead: #29

Hot Flow not triggering awaitItem()

I am attempting to use this library to capture hot flow states after I change them via a function call updating the value. The awaitItem() call never gets the latest item but does get the initial. Am I missing something in my setup of the test?

private val passwordBoolean = flow {
            emit(true)
        }.stateIn(scope, SharingStarted.Lazily, State.Idle)

private val newValueFlow = MutableStateFlow<Int?>(null)
private val loadingFlow = MutableStateFlow(false)

    val state = passwordBoolean
        .flatMapLatest { pwBool ->
            combine(newValueFlow, loadingFlow) { newValue, loading ->
                ViewModelState(
                    passwordState = pwBool,
                    isLoading = loading,
                    newValue = newValue
                )
            }
        }
    .stateIn(
        viewModelScope,
        started = SharingStarted.WhileSubscribed(),
        initialValue = ViewModelState(passwordState = false)
)

fun changeValue(newValue: Int) {
        newValueFlow.value = newValue
}

ViewModelState(
    val passwordState: Boolean,
     isLoading: Boolean = false,
     newValue: Int? = null
)

@Test
    fun `capture update`() = runTest {
        viewModel.state.test {
            assertEquals(false,  awaitItem().passwordState)
            viewModel.changeValue(10)
            assertEquals(10, awaitItem().newValue) // Hangs here
        }
    }

Crashes on kotlinx.coroutines upgrade

After upgrading coroutines package to 1.4.3 i'm getting crashes while using .test.

NoSuchMethodError: kotlinx.coroutines.TimeoutKt.withTimeout

This is basically caused by experimental api usage while using kotlin in lower version then the coroutines library. So when kotlinx.coroutines upgraded to kotlin 1.4.30 all experimental inline-classes API are not compatibile with any library that relies on them. Details: Kotlin/kotlinx.coroutines#2565

Can you release new version with bumped kotlinx.coroutines version asap?

Does not cooperate well with runTest

When using turbine to test flows, delays are not skipped which causes tests to run with full delays. I would expect turbine to skip the delays.

Tested with:

  • Turbine 0.8.0
  • Coroutines 1.6.3

Reproducer:

  @Test
  fun turbineDelay() {
    val delayingFlow = flow {
      delay(2.seconds)
      emit(Unit)
    }
    measureTime {
      runTest {
        delayingFlow.test {
          awaitItem()
          awaitComplete()
        }
      }
    }.also { println("turbine test took $it") }
    measureTime {
      runTest {
        delayingFlow.collect()
      }
    }.also { println("regular runTest took $it") }
  }

This prints on my machine:

turbine test took 2.061758791s
regular runTest took 2.227208ms

Unexpected emissions

Using kotlin 1.6.21, coroutines 1.6.1 and turbine 0.8.0 I noticed the following behavior:

  private fun contentFlow(): Flow<Int> {
    return combine(
      flowOf(Unit),
      flowOf(2).onStart { emit(1) }
    ) { _, b ->
      b
    }.onEach { println(it) }
  }

  @Test
  fun turbineCollect() = runTest {
    contentFlow()
      .test {
        check(awaitItem() == 1)
        cancelAndIgnoreRemainingEvents()
      }
  }

  @Test
  fun regularCollect() = runTest {
    check(contentFlow().toList() == listOf(1, 2))
  }

This turbineCollect test case fails as it only emits the item 2. The regularCollect behaves as expected and collects both the 1 and the 2.

Testing a MutableStateFlow multiple events in order.

Hi,

I got some problem testing my MutableStateFlow.

Heres my code:

  @Test
  fun `deliver should be possible`() = runBlockingTest {
    whenever(repo.validateAddress(any())) doReturn flowOf(Either.Right(addressDeliverable))
    val viewModel = CurrentAddressViewModel(repo)

    //Passes
    flowOf("one", "two").onEach { println(it) }.test {
      assertEquals("one", expectItem())
      assertEquals("two", expectItem())
      expectComplete()
    }

    //Assertion failed
    viewModel.uiState.onEach { println(it) }.test {
      viewModel.validateAddress(addressDeliverable)

      assert(viewModel.getState().value is CurrentAddressUiState.Empty)
      assert(viewModel.getState().value is CurrentAddressUiState.Loading)
      assert(viewModel.getState().value is CurrentAddressUiState.SuccessDeliverPossible)
      expectComplete()
    }
  }

This is the top of the logs:


one
two
se.mat.matse.trx.ui.address.status.CurrentAddressUiState$Empty@649f2009
se.mat.matse.trx.ui.address.status.CurrentAddressUiState$Loading@5bbc9f97
SuccessDeliverPossible(address=Address(streetNumber=, street=, city=, postalCode=, letter=, deliverable=true))

java.lang.AssertionError: Assertion failed

	at se.mat.matse.model.product.CurrentAddressViewModelTest$deliver should be possible$1$4.invokeSuspend(CurrentAddressViewModelTest.kt:73)
	at se.mat.matse.model.product.CurrentAddressViewModelTest$deliver should be possible$1$4.invoke(CurrentAddressViewModelTest.kt)
	at app.cash.turbine.FlowTurbineKt$test$2.invokeSuspend(FlowTurbine.kt:88)
	at app.cash.turbine.FlowTurbineKt$test$2.invoke(FlowTurbine.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:91)
	at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:194)
	at app.cash.turbine.FlowTurbineKt.test-9vOVOX0(FlowTurbine.kt:57)
	at app.cash.turbine.FlowTurbineKt.test-9vOVOX0$default(FlowTurbine.kt:54)
	at se.mat.matse.model.product.CurrentAddressViewModelTest$deliver should be possible$1.invokeSuspend(CurrentAddressViewModelTest.kt:70)
	at se.mat.matse.model.product.CurrentAddressViewModelTest$deliver should be possible$1.invoke(CurrentAddressViewModelTest.kt)

I want to test that it gets the following events in that order Empty, Loading, SuccessDeliverPossible.

If I remove the first two checks and then cancel the rest it works:

    //Assertion now passes
    viewModel.uiState.onEach { println(it) }.test {
      viewModel.validateAddress(addressDeliverable)
      assert(viewModel.getState().value is CurrentAddressUiState.SuccessDeliverPossible)
      cancelAndConsumeRemainingEvents()
    }

Can I use MutableStateFlow this way?

This is the uiState defined in the viewmodel
val uiState = MutableStateFlow<CurrentAddressUiState>(Empty)

Thank you!

Cancellation requirement not enforced for Hot Flows

With Coroutines 1.6.0 and Turbine 0.7.0 the following tests should fail but they actually pass:

class TestHotFlowWithoutCancellingFlowTurbine {
    @Test
    fun `test runTest + StandardTestDispatcher`() = runTest(StandardTestDispatcher()) {
        testHotFlowsWithoutCancellingFlowTurbine()
    }

    @Test
    fun `test runTest + UnconfinedTestDispatcher`() = runTest(UnconfinedTestDispatcher()) {
        testHotFlowsWithoutCancellingFlowTurbine()
    }

    @Test
    fun `test runBlockingTest`() = runBlockingTest {
        testHotFlowsWithoutCancellingFlowTurbine()
    }

    @Test
    fun `test runBlocking`() = runBlocking {
        testHotFlowsWithoutCancellingFlowTurbine()
    }

    private suspend fun testHotFlowsWithoutCancellingFlowTurbine() {
        val sharedFlow = MutableSharedFlow<Int>()
        sharedFlow.test {
            sharedFlow.emit(0)
            assertEquals(0, awaitItem())
        }

        val stateFlow = MutableStateFlow(0)
        stateFlow.test {
            assertEquals(0, awaitItem())
        }

        val channel = Channel<Int>()
        channel.consumeAsFlow().test {
            channel.send(0)
            assertEquals(0, awaitItem())
        }
    }
}

Ideally these tests would require a call to either cancelAndConsumeRemainingEvents() or cancelAndIgnoreRemainingEvents() towards the end of each test { โ€ฆ } block.

Flow tests do not work on Kotlin 1.4.10 JVM

Hello, I am trying to use this library in our Android app using Kotlin 1.4.10, unfortunately even when trying to perform a basic sample test like this one

flowOf("one", "two").test {
  assertEquals("one", expectItem())
  assertEquals("two", expectItem())
  expectComplete()
}

I get an exception:
java.lang.NoSuchMethodError: kotlinx.coroutines.channels.ChannelKt.Channel$default(ILkotlinx/coroutines/channels/BufferOverflow;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/channels/Channel;

I use InstantTaskExecutorRule and blocking CoroutineRule to mock things.

Figure out if we want automatic cancelation at the end of a test block

Right now a test block requires that you cancel, cancel and ignore events, or consume a terminal event before the test block will complete successfully. This creates a very explicit script that details the lifecycle of the underlying collection operation, but with infinite flows (such as those from SQLDelight or presenters) it means every block ends with cancel().

What are the implications of automatically canceling? We would still validate all received events were consumed. cancel() would almost certainly still be an API so that you could explicitly control it. We could rename cancelAndIgnoreRemainingEvents() to just ignoreRemainingEvents().

java.lang.NoSuchMethodError: 'java.lang.Object kotlinx.coroutines.TimeoutKt.withTimeout-KLykuaI(long, kotlin.jvm.functions.Function2, kotlin.coroutines.Continuation)'

I have created this test, which is a copy/paste from a test in your documentation:

@ExperimentalCoroutinesApi
@ExperimentalTime
@Test
fun `my test`() = runBlockingTest {
    val mutableSharedFlow = MutableSharedFlow<Int>(replay = 0)
    mutableSharedFlow.test {
        mutableSharedFlow.emit(1)
        assertEquals(actual = awaitItem(), expected = 1)
        cancelAndConsumeRemainingEvents()
    }
}

However, when I run it, I keep getting the following error:

'java.lang.Object kotlinx.coroutines.TimeoutKt.withTimeout-KLykuaI(long, kotlin.jvm.functions.Function2, kotlin.coroutines.Continuation)'
java.lang.NoSuchMethodError: 'java.lang.Object kotlinx.coroutines.TimeoutKt.withTimeout-KLykuaI(long, kotlin.jvm.functions.Function2, kotlin.coroutines.Continuation)'
	at app.cash.turbine.ChannelBasedFlowTurbine.withTimeout(FlowTurbine.kt:228)
	at app.cash.turbine.ChannelBasedFlowTurbine.awaitEvent(FlowTurbine.kt:260)
	at app.cash.turbine.ChannelBasedFlowTurbine.awaitItem(FlowTurbine.kt:266)
	at SettingsDialogViewModelTest$my test$1$1.invokeSuspend(SettingsDialogViewModelTest.kt:25)
	at SettingsDialogViewModelTest$my test$1$1.invoke(SettingsDialogViewModelTest.kt)
	at SettingsDialogViewModelTest$my test$1$1.invoke(SettingsDialogViewModelTest.kt)
	at app.cash.turbine.FlowTurbineKt$test$2.invokeSuspend(FlowTurbine.kt:86)
	at app.cash.turbine.FlowTurbineKt$test$2.invoke(FlowTurbine.kt)
	at app.cash.turbine.FlowTurbineKt$test$2.invoke(FlowTurbine.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:91)
	at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:194)
	at app.cash.turbine.FlowTurbineKt.test-dWUq8MI(FlowTurbine.kt:55)
	at app.cash.turbine.FlowTurbineKt.test-dWUq8MI$default(FlowTurbine.kt:51)
	at SettingsDialogViewModelTest$my test$1.invokeSuspend(SettingsDialogViewModelTest.kt:23)
	at SettingsDialogViewModelTest$my test$1.invoke(SettingsDialogViewModelTest.kt)
	at SettingsDialogViewModelTest$my test$1.invoke(SettingsDialogViewModelTest.kt)
	at kotlinx.coroutines.test.TestBuildersKt$runBlockingTest$deferred$1.invokeSuspend(TestBuilders.kt:50)
	(Coroutine boundary)
	at SettingsDialogViewModelTest$my test$1.invokeSuspend(SettingsDialogViewModelTest.kt:23)
	at kotlinx.coroutines.test.TestBuildersKt$runBlockingTest$deferred$1.invokeSuspend(TestBuilders.kt:50)

My dependencies include

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.5.2")
testImplementation("app.cash.turbine:turbine:0.6.1")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2")

I am confident the test is fine since it is a copy/paste from your documentation. Not sure if I am missing any dependency or some other configuration. I would appreciate some guidance.

Not sure if the following information is relevant to better understand the problem but the project I am working is not an Android app. It is a plugin for the IntelliJ IDE written in Kotlin and it has some Swing too.

Thank you

JS legacy support

Hey, 0.9 doesn't contain support for JS legacy. Is there a specific reason for this, like bugs?
I mean, turbine is useful at my side testing rsocket-kotlin multiplatform library, and as Legacy is till default JS target and still will be until kotlin 1.8 it would be good to support it

An example on the documentation doesn't work

In https://github.com/cashapp/turbine#asynchronous-flows, we get this example :

flowOf("one", "two", "three")
  .map {
    delay(100)
    it
  }
  .test {
    // 0 - 100ms -> no emission yet
    // 100ms - 200ms -> "one" is emitted
    // 200ms - 300ms -> "two" is emitted
    // 300ms - 400ms -> "three" is emitted
    delay(250)
    assertEquals("two", expectMostRecentItem())
    cancelAndIgnoreRemainingEvents()
  }

Unfortunately, it doesn't work with a basic kotlin-coroutine-testing v1.6 runTest :

  @Test
  fun `doesn't work`() = runTest {
    flowOf("one", "two", "three")
      .map {
        delay(100)
        it
      }
      .test {
        // 0 - 100ms -> no emission yet
        // 100ms - 200ms -> "one" is emitted
        // 200ms - 300ms -> "two" is emitted
        // 300ms - 400ms -> "three" is emitted
        delay(250)
        assertEquals("two", expectMostRecentItem())
        cancelAndIgnoreRemainingEvents()
      }
  }

java.lang.AssertionError: No item was found is the error.

It would be great to re-write the documentation since kotlin-coroutine-testing v1.6 changed everything (and not for the best).

How to use this with runTest

I've seen in the issues that there are some workarounds for getting Turbine to work with runTest, I can't get it to work properly and not take real time. Does the library actually support using runTest, and is there any documentation on how to use it without workarounds?

AdvanceTimeBy has no effect within test block when using runBlockingTest

Pretty simple repro with the following test:

    @Test
    fun test() = runBlockingTest {
        val flow = flow {
            delay(100L)
            emit("a")
        }

        flow.test {
            advanceTimeBy(101L)
            assertThat(expectItem()).isEqualTo("a")
        }
    }

However, the test above fails with the following error:

kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms
	at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:186)
	at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:156)
	at kotlinx.coroutines.test.TimedRunnable.run(TestCoroutineDispatcher.kt)
	at kotlinx.coroutines.test.TestCoroutineDispatcher.doActionsUntil(TestCoroutineDispatcher.kt:103)
	at kotlinx.coroutines.test.TestCoroutineDispatcher.advanceUntilTime(TestCoroutineDispatcher.kt:123)
	at kotlinx.coroutines.test.TestCoroutineDispatcher.advanceUntilIdle(TestCoroutineDispatcher.kt:133)
	at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:52)
	at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest$default(TestBuilders.kt:45)
	at com.repro.ReproTest.test(ReproTest.kt:16)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)

I suspect this is probably some nuance with TestCoroutineDispatcher plus the fact that test { โ€ฆ } is internally using Dispatchers.Unconfined.

Kotlin: 1.4.31
Coroutines: 1.4.3
Turbine: 0.4.1

Cannot test SharedFlow

I have an android viewmdeol class with the following property

private val _trainingNavigationEvents = MutableSharedFlow<NavigationEventTraining>(replay = 0)
    val trainingNavigationEvents = _trainingNavigationEvents.asSharedFlow()

fun navigate(navigationEvent: NavigationEventTraining) {
        viewModelScope.launch {
            _trainingNavigationEvents.emit(navigationEvent)
        }
    }

I am using a SharedFlow as it solves the SingleLiveEvent problem.

The issue arises when I try and unit test the code. I can't see how to use turbine (or supplied primitives) to get it to work.

    @ExperimentalTime
    @Test
    fun `navigate`() = runBlockingTest {
        viewModel.handleIntent(TrainingViewModel.TrainingIntent.ShowQuestions)

        viewModel.navigationEvents.test {
            assertEquals(
                TrainingViewModel.TrainingNavigationEvent.NavigateToQuestions::class,
                expectItem()::class
            )
            cancelAndConsumeRemainingEvents()
        }
    }

and I get

kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms

I know that a SharedFlow never completes and that may be part of the reason but I have been unable to find any examples of how to do this instead.

I am using Junit 5 and am using a TestCoroutineDispatcher class extension.

Missing emissions when using combine operator

I'm getting an unexpected result when checking flow emissions after using the combine operator. Using .test{} results in less emissions than using .collect{}. I expect .test{} to result in the same # of emissions.

If this is indeed considered a bug and not a misuse from my side, I'd be happy to try and contribute a fix.

Reproduction test (based on the combine operator docs):

@Test
fun combineTurbine() = runBlockingTest {
    combine(flow1(), flow2()) { i, s -> i.toString() + s }.collect {
        println("Collect: $it") // Will print "1a 2a 2b 2c"
    }

    combine(flow1(), flow2()) { i, s -> i.toString() + s }.test {
        println("Turbine test: ${expectItem()}") // Will print 2b
        println("Turbine test: ${expectItem()}") // Will print 2c
    }
}

private fun flow1() = flowOf(1, 2)
private fun flow2() = flowOf("a", "b", "c")

Output:

Collect: 1a
Collect: 2a
Collect: 2b
Collect: 2c
Turbine test: 2b
Turbine test: 2c

Running:

  • Turbine version 0.4.0
  • Kotlin version 1.4.30
  • Coroutines version 1.4.1

NoSuchMethodError using kotlin 1.4.30-M1

Hi there,

I am wondering if 0.30 will support 1.4.30-M1. I got this error related to turbine:

java.lang.NoSuchMethodError: 'java.lang.Object app.cash.turbine.FlowTurbineKt.test-dWUq8MI$default(kotlinx.coroutines.flow.Flow, double, kotlin.jvm.functions.Function2, kotlin.coroutines.Continuation, int, java.lang.Object)'
	at com.squareup.FooTest$not visible when XXX flag is off$1.invokeSuspend(TeamManagementAddOnCategoryTest.kt:58)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:84)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at com.squareup.FooTest.not visible when XXX flag is off(FooTest.kt:57)```

Different behaviour when using coroutines 1.6.0

As an example I would like to suggest following test:

private val stateFlow = MutableStateFlow(0)
private val flow: Flow<Int> = stateFlow

@Test
fun `turbine test`() = runBlockingTest {
    Dispatchers.setMain(TestCoroutineDispatcher())

    val scope = CoroutineScope(Job() + Dispatchers.Main)

    flow.test {
        flowOf(1, 2, 3)
            .onEach { stateFlow.value = it }
            .launchIn(scope)

        val events = cancelAndConsumeRemainingEvents()
    }

    Dispatchers.resetMain()
}

When using coroutines 1.5.2, as expected events variable will be a list with size of 4 which contains integers wrapped in Item class: [Item(0), Item(1), Item(2), Item(3)]

But when using coroutines 1.6.0 like so:

private val stateFlow = MutableStateFlow(0)
private val flow: Flow<Int> = stateFlow

@Test
fun `turbine test`() = runTest {
    Dispatchers.setMain(UnconfinedTestDispatcher())

    val scope = CoroutineScope(Job() + Dispatchers.Main)

    flow.test {
        flowOf(1, 2, 3)
            .onEach { stateFlow.value = it }
            .launchIn(scope)

        val events = cancelAndConsumeRemainingEvents()
    }

    Dispatchers.resetMain()
}

events will contain only the first and last items: [Item(0), Item(3)].

Also if I use StandardTestDispatcher instead of UnconfinedTestDispatcher, events will contain only the first item: [Item(0)].

Is it expected behaviour? Do I need to adjust my test somehow to get the same results as for coroutines version 1.5.2?

Question: Is there a way to test no events emitted in the flow?

I have a function like:

val uiEvents = StateFlow<UIEvent>(Loading)

fun checkAppVersion() {
  viewModelScope.launch {
    val minRequiredAppVersion = repository.getRequireAppVerion()
    if (minRequiredAppVersion > currentAppVersion) {
      uiEvents.emit(ShowUpgradeDialog)
    }
  }
}

It was easy to test the case when there should be an upgrade event.
I can not find a way how to test the case where is no upgrade situation.

Later after the loading of the data UI state is changed to some data presentation. So I can check that loading changed to data state. But I wonder if there is a possibility to test this small bit that no events are passed to the flow?
Is it not an important test?

`should get schedules` test fail with Expected item but found Complete

@Before
    fun setup() {
        MockKAnnotations.init(this)
        Dispatchers.setMain(dispatcher)
        searchSchedulesViewModel = SearchSchedulesViewModel()
    }
 fun `should get error`() = runBlockingTest {
        // Arrange
        every { searchRepository.getSchedulesStreamBy("Game") } answers {
            flow { PagingData.from(listOf(Throwable())) }
        }

        // Act & Assert
        searchSchedulesViewModel.searchSchedules("Game").onEach { println("Flow contains $it") }.test {
            expectError()
        }
    }
class DefaultOcsRepository(private val ocsService: OcsService) : OcsRepository {
   override fun getSchedulesStreamBy(title: String): Flow<PagingData<Schedule>> =
       Pager(
           config = PagingConfig(
               pageSize = NETWORK_PAGE_SIZE,
               enablePlaceholders = false
           ), pagingSourceFactory = { OcsPagingSource(ocsService, title) }
       )
           .flow.map {
               it.map { scheduleResponse: ScheduleResponse ->
                   Schedule(
                       scheduleResponse.id ?: "",
                       scheduleResponse.title!![0].value ?: "",
                       scheduleResponse.subtitle ?: "",
                       scheduleResponse.imageurl ?: ""
                   )
               }
           }

   companion object {
       private const val NETWORK_PAGE_SIZE = 30
   }
}

The issue will be gone if I change the ViewModel constructor to have a repository as a parameter but as result, the bellow test will fail:

@Test
   fun `should get schedules`() = runBlockingTest {
       // Arrange
       every { searchRepository.getSchedulesStreamBy("Game") } answers {
           flow { PagingData.from(listOf(Schedule("1", "Game of", "drama", "url"))) }
       }

       // Act & Assert
       searchSchedulesViewModel.searchSchedules("Game").test {
           expectItem()
       }
   }

NoSuchMethodError in Android project with Kotlin IR compiler enabled

When I run unit tests that use turbine while Kotlin's IR compiler is enable the tests fail with a NoSuchMethodError.

Stack trace:

java.lang.NoSuchMethodError: app.cash.turbine.FlowTurbineKt.test-f_gJSvk$default(Lkotlinx/coroutines/flow/Flow;DLkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
	at com.example.myapplication.ExampleUnitTest$failingTest$1.invokeSuspend(ExampleUnitTest.kt:21)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:84)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at com.example.myapplication.ExampleUnitTest.failingTest(ExampleUnitTest.kt:20)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
	at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
	at com.sun.proxy.$Proxy2.processTestClass(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.processTestClass(TestWorker.java:119)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:182)
	at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:164)
	at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:414)
	at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
	at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56)
	at java.lang.Thread.run(Thread.java:748)

Example project that reproduces the issue
MyApplication2.zip

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.