Vincent Williams
11/03/2019, 7:25 PMcollect { }
in the unit test and then verify that the correct item comes through, however this throws IllegalStateException: This job has not completed yet
Mark Murphy
11/03/2019, 7:27 PMJob
from your coroutine builder, then cancel()
the Job
at the bottom of the unit test.Vincent Williams
11/03/2019, 7:28 PMfun test() = testDispatcher.runBlockingTest {
//test stuff here
testDispatcher.cancel()
}
Vincent Williams
11/03/2019, 7:28 PMVincent Williams
11/03/2019, 7:29 PMrunBlocking
instead of runBlockingTest
my test hangsMark Murphy
11/03/2019, 7:31 PMfun test() = runBlockingTest {
val job = launch {
flow.collect { TODO() }
}
// rest of stuff here
job.cancel()
}
I don't know that this is the best solution, but it is working for me.Vincent Williams
11/03/2019, 7:34 PMrunBlockingTest
I will try that for now but I would like to know if anyone has found an easier wayVincent Williams
11/03/2019, 7:35 PMchannel.test()
.assertValue(someValue)
Vincent Williams
11/03/2019, 7:35 PMoctylFractal
11/03/2019, 8:03 PMDragan StanojeviÄ - Nevidljivi
11/03/2019, 9:28 PM@ExperimentalCoroutinesApi
class CoroutineChannelExampleTests {
private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
private var mockedResources: Resources = mock()
private val osRuntimeValues = OSRuntimeValues(isDebuggingEnabled = true)
private var logger = object: InstallerAppLogger(osRuntimeValues) {
override fun logToSystem(logLevel: LogLevel, applicationTag: String, classTag: String, message: String) {
println("$classTag:: $message") // override logger's logToSystem() to use println while testing
}
}
private var dummyPersistence = spy(DummyPersistence())
private val viewModel = YourViewModel(
language = "DE",
resources = mockedResources,
osRuntimeValues = osRuntimeValues,
logger = logger,
persistence = dummyPersistence
)
private val eventChannelSubscription = viewModel.getEventsChannel().openSubscription()
private var event: YourViewModel.Event? = null
private var messageReceived = false
private var fragmentChangedEvent: FragmentChanged? = null
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher
testDispatcher.cleanupTestCoroutines()
}
@Test
fun `test happy path`() = testDispatcher.runBlockingTest {
val eventsChannelListener = initEventsChannelListener()
viewModel.getCategories()
waitForEvent()
verify(dummyPersistence).getCategories()
fragmentChangedEvent = event as FragmentChanged
assertThat(fragmentChangedEvent!!.fragmentType).isEqualTo(CATEGORIES)
assertThat(viewModel.selectedCategory).hasSize(2)
assertThat(viewModel.selectedCategory).isEqualTo(dummyPersistence.getMockedCategoryUIModels())
eventChannelSubscription.cancel()
while (!eventsChannelListener.isCompleted)
yield()
}
// helper method to initialise EventChannelListener in each test
private fun TestCoroutineScope.initEventsChannelListener(): Job {
return launch {
eventChannelSubscription.consumeEach {
while (messageReceived) yield() // force consuming one-by-one
event = it
messageReceived = true
}
}
}
// helper function used for waiting for an event from coroutines channel
private suspend fun waitForEvent() {
while (!messageReceived) yield()
messageReceived = false
}
}
Most interesting portions are: initEventsChannelListener()
, waitForEvent()
calls, canceling the subscription at the end of tests and waiting at the end of @test fun with:
while (!eventsChannelListener.isCompleted)
yield()
Some past code sample from Google in testing multiple threads also used latches with similarly ugly code, but, again, there has to be a better way to do it...
I hoped @Manuel Vivo @Sean McQuillan [G] would have touched this in part of testing coroutines, since it's not trivial, but quite essential in any ViewModel testing...Dragan StanojeviÄ - Nevidljivi
11/03/2019, 9:28 PMIllegalStateException: This job has not completed yet
.Mark Murphy
11/03/2019, 9:31 PMI don't think so, since with launch {} inside @test fun you're launching another coroutine which may, in this case usually does, run longer then the @test funSince I
cancel()
the Job
before the test function ends, AFAIK the coroutine should not run longer than the test function. Admittedly, this assumes cooperative behavior, which may not exist in some cases.Dragan StanojeviÄ - Nevidljivi
11/03/2019, 9:32 PMDragan StanojeviÄ - Nevidljivi
11/03/2019, 9:34 PMDragan StanojeviÄ - Nevidljivi
11/03/2019, 9:36 PMVincent Williams
11/03/2019, 11:24 PMlaunch
inside of runBlockingTest
if I dont do that it hangs or shows This job has not completed yet
Vincent Williams
11/03/2019, 11:24 PMGiorgos Neokleous
11/04/2019, 9:37 AMrunBlockingTest
or any of the coroutines test dependencies. Is to take(numberOfEmissions)
before collection. This way my test run successfully. You can even do collectIndexed { index, value-> }
to have different assertions for different emissionsVincent Williams
11/04/2019, 3:50 PMcollect
or collectIndexed
are suspend functionsGiorgos Neokleous
11/04/2019, 3:55 PMrunBlocking
Vincent Williams
11/04/2019, 3:56 PMrunBlocking
results in a test that hangsGiorgos Neokleous
11/04/2019, 3:59 PMVincent Williams
11/04/2019, 3:59 PMGiorgos Neokleous
11/04/2019, 4:01 PM.take(numberOfEmissions)
before collecting your flow should do the trickVincent Williams
11/04/2019, 5:03 PMVincent Williams
11/04/2019, 5:03 PM