I have a reasonably simple bit of code - every second ping some handler with the current timestamp:
private val clock = Clock.systemDefaultZone()
private fun now() = clock.instant()
suspend fun ping(pinger: suspend (Instant) -> Unit) {
repeat(5) {
pinger(now())
if (it < 4) {
delay(1.seconds)
}
}
}
Which I want to expose as a reactive Producer:
fun publishPing() = publish { ping(::send) }
In practice, it works - but when testing:
#OptIn(ExperimentalCoroutinesApi::class)
#Test
fun `test publishPing`() = runTest {
var count = 0
publishPing().collect {
count++
assertTrue { EPOCH.isBefore(it) }
assertTrue { it.isBefore(now()) }
}
assertEquals(5, count) // Pass
assertEquals(4000, currentTime) // AssertionFailedError: Expected:4000; Actual:0
}
The virtual time doesn't get incremented, and he test takes 4+ seconds to run (eg the calls to delay() aren't being handled by the test dispatcher).
Doing the same with a flow all works as expected (and runs in milliseconds).
fun flowPing() = flow { ping(::emit) }
#OptIn(ExperimentalCoroutinesApi::class)
#Test
fun `test flowPing`() = runTest {
var count = 0
flowPing().collect {
count++
assertTrue { EPOCH.isBefore(it) }
assertTrue { it.isBefore(now()) }
}
assertEquals(5, count) // Pass
assertEquals(4000, currentTime) // Pass
}
I have a vague idea of what is [not] going on (the coroutines-test support isn't Hooked into the coroutines/reactive support?) - but what do I need to do to sort it?
FWIW the same behaviour happens with coroutines 1.5.x and runBlockingTest (code above from coroutines 1.6 and runTest)
Related
I wrote a very simple slowdown function that can be used to slow down a response without blocking
suspend inline fun <T> slowdown(duration: Duration, block: () -> T): T {
var result: T
val timeTaken = measureTime {
result = block()
}
delay(duration - timeTaken)
return result
}
running it inside a main works correctly
fun main() = runBlocking {
val timeTaken1 = measureTime {
slowdown(2.seconds) {
delay(1000)
}
}
val timeTaken2 = measureTime {
slowdown(2.seconds) {
delay(4000)
}
}
println(timeTaken1) // 2.008962891s
println(timeTaken2) // 4.000145711s
}
Now I want to write a test case that will test the slowdown, but without actually taking 2 seconds or 4 seconds
#Test
#ExperimentalCoroutinesApi
fun `ensure execution takes N seconds when operation takes N-t time`() = runTest {
val timeTaken1 = measureTime {
slowdown(2.seconds) {
delay(1000)
}
}
val timeTaken2 = measureTime {
slowdown(2.seconds) {
delay(4000)
}
}
println("========")
println(timeTaken1) // 9.472240ms
println(timeTaken2) // 131.839us
println("========")
}
I'm assuming measureTime doesn't play nice with runTest, but works great with delay
If I replace delay with Thread.sleep in the test
val timeTaken1 = measureTime {
slowdown(2.seconds) {
Thread.sleep(1000)
}
}
val timeTaken2 = measureTime {
slowdown(2.seconds) {
Thread.sleep(4000)
}
}
then it prints out
========
1.008311216s
4.000221902s
========
delay(1000) happens instantly under runTest, but measureTime also measures it as instantly instead of being offset by 1 second.
I thought I could hack it using advanceTimeBy(), but it doesn't seem to affect measureTime.
Changing runTest to runBlocking works correctly, but it also slows down the tests to 2 and 4 seconds respectively.
Is there a measureTime that plays nicely with runTest and works under normal runBlocking ?
suspend fun <T> slowdown(duration: Duration, block: suspend () -> T): T = coroutineScope {
launch { delay(duration) }
block()
}
#Test
#ExperimentalCoroutinesApi
fun `ensure execution takes N seconds when execution faster than N`() = runTest {
val currentTime1 = currentTime
slowdown(2.seconds) {
delay(1.seconds)
}
val currentTime2 = currentTime
assertEquals(2000, currentTime2 - currentTime1)
}
#Test
#ExperimentalCoroutinesApi
fun `ensure execution takes more than N seconds when execution slower than N`() = runTest {
val currentTime1 = currentTime
slowdown(1.seconds) {
delay(2.seconds)
}
val currentTime2 = currentTime
assertEquals(2000, currentTime2 - currentTime1)
}
I'm trying to write a component which uses different datasources of data.
Then data is combined and emitted in the different resulting flow.
class TaskControlComponent(
private val diskCacheDataSource: DiskCacheDataSource,
private val debugDataSource: DebugDataSource
) {
private val _localTasks = MutableStateFlow<Map<String, TaskItem>>(emptyMap())
val localTasks: StateFlow<Map<String, TaskItem>> = _localTasks
suspend fun loadLocal() {
flowOf(
diskCacheDataSource.defaultFeatures,
diskCacheDataSource.localFeatures,
debugDataSource.debugFeatures
).flattenMerge().collect {
computeLocalTasks()
}
}
private suspend fun computeLocalTasks() {
val resultTasks = HashMap<String, TaskItem>(64)
listOf(
diskCacheDataSource.defaultTasks,
diskCacheDataSource.localTasks,
debugDataSource.debugTasks
).forEach { tasksMap ->
tasksMap.value.forEach { entry ->
resultTasks[entry.key] = entry.value
}
}
_localTasks.emit(resultTasks)
}
}
DataSource
interface DiskCacheDataSource {
val defaultTasks: StateFlow<Map<String, TaskItem>>
val localTasks: StateFlow<Map<String, TaskItem>>
}
It works, but how to write junit test for that?
class TaskControlImplTest {
private lateinit var taskControl: TaskControlComponent
#Mock
lateinit var diskCacheDataSource: DiskCacheDataSource
#Mock
lateinit var debugDataSource: DebugDataSource
#Before
fun setup() {
MockitoAnnotations.initMocks(this)
taskControl = TaskControlComponent(diskCacheDataSource, debugDataSource)
}
#Test
fun testFeatureControl() {
whenever(diskCacheDataSource.defaultTasks).thenReturn(
MutableStateFlow(
mapOf(
"1" to TaskItem(
"1",
TaskStatus.On
)
)
)
)
whenever(diskCacheDataSource.localTasks).thenReturn(MutableStateFlow(emptyMap()))
whenever(debugDataSource.debugTasks).thenReturn(MutableStateFlow(emptyMap()))
runBlocking {
taskControl.loadLocal()
}
runBlocking {
taskControl.localTasks.collect {
Assert.assertEquals(it.size, 1)
}
}
}
}
In case of the following sequence of commands
runBlocking {
taskControl.loadLocal()
}
runBlocking {
taskControl.localTasks.collect {
Assert.assertEquals(it.size, 1)
}
}
test freezes, and runs forewer.
When I swap the pieces of code, first instead of second and the contrary
runBlocking {
featureControl.localFeatures.collect {
Assert.assertEquals(it.size, 1)
}
}
runBlocking {
featureControl.loadLocal()
}
Tests finishes with warning
expected:<0> but was:<1>
Expected :0
Actual :1
Is it possible to write test for such usecase? What should be investigated or done in order to make test workable?
The reason the order matters here is because StateFlow is hot, unlike normal Flow, meaning it starts running with data immediately not when it is collected
I test with the turbine library but you don't need it. I don't remember the exact setup of not using turbine but it was a bit more complicated so I chose to use turbine
https://github.com/cashapp/turbine
I want to collect specific amount of values from Flow until value emitting timeout happened. Unfortunately, there are no such operators, so I've tried to implement my own using debounce operator.
The first problem is that producer is too fast and some packages are skipped and not collected at all (they are in onEach of original packages flow, but not in onEach of second flow of merge in withNullOnTimeout).
The second problem - after taking last value according to amount argument orginal flow is closed, but timeout flow still alive and finally produce timeout after last value.
How can I solve this two problems?
My implementations:
suspend fun receive(packages: Flow<ByteArray>, amount: Int): ByteArray {
val buffer = ByteArrayOutputStream(blockSize.toInt())
packages
.take(10)
.takeUntilTimeout(100) // <-- custom timeout operator
.collect { pck ->
buffer.write(pck.data)
}
return buffer.toByteArray()
}
fun <T> Flow<T>.takeUntilTimeout(durationMillis: Long): Flow<T> {
require(durationMillis > 0) { "Duration should be greater than 0" }
return withNullOnTimeout(durationMillis)
.takeWhile { it != null }
.mapNotNull { it }
}
fun <T> Flow<T>.withNullOnTimeout(durationMillis: Long): Flow<T?> {
require(durationMillis > 0) { "Duration should be greater than 0" }
return merge(
this,
map<T, T?> { null }
.onStart { emit(null) }
.debounce(durationMillis)
)
}
This was what initially seemed obvious to me, but as Joffrey points out in the comments, it can cause an unnecessary delay before collection terminates. I'll leave this as an example of a suboptimal, oversimplified solution.
fun <T> Flow<T>.takeUntilTimeout(durationMillis: Long): Flow<T> = flow {
val endTime = System.currentTimeMillis() + durationMillis
takeWhile { System.currentTimeMillis() >= endTime }
.collect { emit(it) }
}
Here's an alternate idea I didn't test.
#Suppress("UNCHECKED_CAST")
fun <T> Flow<T>.takeUntilTimeout(durationMillis: Long): Flow<T> {
val signal = Any()
return merge(flow { delay(durationMillis); emit(signal) })
.takeWhile { it !== signal } as Flow<T>
}
How about:
fun <T> Flow<T>.takeUntilTimeout(timeoutMillis: Long) = channelFlow {
val collector = launch {
collect {
send(it)
}
close()
}
delay(timeoutMillis)
collector.cancel()
close()
}
Using a channelFlow allows you to spawn a second coroutine so you can count the time independently, and quite simply.
Why does the first code snippet produces Inappropriate blocking method call warning but not the second one?
private fun prepareList() = launch {
withContext(Dispatchers.IO) {
requireContext().openFileOutput(listFileName, Application.MODE_PRIVATE).use { out ->
requireContext().assets.open(listFileName).use {
it.copyTo(out)
}
}
}
}
private fun prepareList() = launch(Dispatchers.IO) {
requireContext().openFileOutput(listFileName, Application.MODE_PRIVATE).use { out ->
requireContext().assets.open(listFileName).use {
it.copyTo(out)
}
}
}
I am trying to test the getTopicNames function (below) in two scenarios: If it succeeds and if it does not succeed.
fun getTopicNames(): Either<Exception, Set<String>> =
try {
adminClient.listTopics()
.names()
.get()
.right()
} catch (exception: ExecutionException) {
exception.left()
}
This is the test class in which I am doing those two scenarios. If I run each test individually, they both suceed. If I run the entire class the second to execute fails because for some reason the previous mock on adminClient.listTopics() is being retained.
These are the versions for everything involved:
kotlin: 1.3.72
koin: 2.1.6
junit: 5.6.1
mockk: 1.10.0
class TopicOperationsTest {
#BeforeEach
fun start() {
val testModule = module(createdAtStart = true) {
single { mockk<AdminClient>() }
}
startKoin { modules(testModule) }
}
#AfterEach
fun stop() {
stopKoin()
}
#Test
fun `getTopicNames() returns a Right with the topics names`() {
val adminClient = get(AdminClient::class.java)
val listOfTopicsToReturn = mockk<ListTopicsResult>()
val expectedTopics = setOf("Topic1", "Topic2", "Topic3")
every { adminClient.listTopics() } returns listOfTopicsToReturn
every { listOfTopicsToReturn.names() } returns KafkaFuture.completedFuture(expectedTopics)
println("listOfTopicsToReturn.names(): " + listOfTopicsToReturn.names())
println("adminClient.listTopics(): " + adminClient.listTopics())
println("getTopicNames(): " + getTopicNames())
assertThat(getTopicNames().getOrElse { emptySet() }, `is`(expectedTopics))
}
#Test
fun `getTopicNames() returns a Left if failing to get topic names`() {
val adminClient = get(AdminClient::class.java)
every { adminClient.listTopics() } throws ExecutionException("Some Failure", Exception())
assertThat(getTopicNames(), IsInstanceOf(Either.Left::class.java))
}
}
This is the error I get, caused by the fact that the test that verifies the failure is the first to run:
java.lang.AssertionError:
Expected: is <[Topic1, Topic2, Topic3]>
but: was <[]>
Expected :is <[Topic1, Topic2, Topic3]>
Actual :<[]>
<Click to see difference>
Already tried clearAllMocks() on the BeforeEach method but it does not solve my problem as I just start getting:
io.mockk.MockKException: no answer found for: AdminClient(#1).listTopics()
I found a solution that makes everything work. It is a combination of:
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
Having the mock as a class object
MockKAnnotations.init(this) in the #BeforeEach method
clearMocks() specifying the actual mock to be cleared (should work for multiple mocks too, just separated by commas.
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TopicOperationsTest {
private var adminClientMock = mockk<AdminClient>()
#BeforeEach
fun start() {
MockKAnnotations.init(this)
val testModule = module(createdAtStart = true) {
single { adminClientMock }
}
startKoin { modules(testModule) }
}
#AfterEach
fun stop() {
clearMocks(adminClientMock)
stopKoin()
}
#Test
fun `getTopicNames() returns a Right with the topics names`() {
val adminClient = get(AdminClient::class.java)
val listOfTopicsToReturn = mockk<ListTopicsResult>()
val expectedTopics = setOf("Topic1", "Topic2", "Topic3")
every { adminClient.listTopics() } returns listOfTopicsToReturn
every { listOfTopicsToReturn.names() } returns KafkaFuture.completedFuture(expectedTopics)
assertThat(getTopicNames().getOrElse { emptySet() }, `is`(expectedTopics))
}
#Test
fun `getTopicNames() returns a Left if failing to get topic names`() {
val adminClient = get(AdminClient::class.java)
every { adminClient.listTopics() } throws ExecutionException("Some Failure", Exception())
assertThat(getTopicNames(), IsInstanceOf(Either.Left::class.java))
}
}