runTest & TestScope (virtualized-time in tests)
From with-runTest
Go to text ā
Example using runTest to control virtual time in tests.
Code
package com.glassthought.sandbox
import com.glassthought.sandbox.impl.CustomDescribeSpec
import com.glassthought.sandbox.util.out.impl.out
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/** Shows basics of using runTest so that functions like 'delay' become virtual timed. */
@OptIn(ExperimentalCoroutinesApi::class)
class VirtualTimeDescribeSpec : CustomDescribeSpec({
describe("Timer Service") {
it("should complete instantly with virtual time") {
runTest {
var done = false
out.info("advanceUntilIdle() auto advances")
launch {
out.delay(5.seconds) // Would normally take 5 seconds
done = true
}
// Time jumps forward instantly
advanceUntilIdle()
done shouldBe true
}
}
it("should control time step by step") {
runTest {
var counter = 0
out.info("advanceTimeBy() gives precise control.")
launch {
out.delay(1.seconds)
counter++
out.delay(1.seconds)
counter++
}
advanceTimeBy(1100.milliseconds)
counter shouldBe 1
advanceTimeBy(1100.milliseconds)
counter shouldBe 2
}
}
}
})
private var count = 0
private suspend fun incrementCountAndPrint(string: String) {
count += 1
out.info("[${count}]: ${string}")
}
private suspend fun incrementAndPrintForIt(msg: String) {
incrementCountAndPrint(msg)
out.info("")
}
Command to reproduce:
gt.sandbox.checkout.commit 680dd5e9e988c8ebec90 \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./gradlew test --rerun-tasks"
Recorded output of command:
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
> Task :app:checkKotlinGradlePluginConfigurationErrors SKIPPED
> Task :app:processResources NO-SOURCE
> Task :app:processTestResources NO-SOURCE
> Task :app:compileKotlin
> Task :app:compileJava NO-SOURCE
> Task :app:classes UP-TO-DATE
> Task :app:compileTestKotlin
> Task :app:compileTestJava NO-SOURCE
> Task :app:testClasses UP-TO-DATE
> Task :app:test
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
Gradle Test Executor 1 STANDARD_OUT
Warning: Kotest autoscan is enabled. This means Kotest will scan the classpath for extensions that are annotated with @AutoScan. To avoid this startup cost, disable autoscan by setting the system property 'kotest.framework.classpath.scanning.autoscan.disable=true'. In 6.0 this value will default to true. For further details see https://kotest.io/docs/next/framework/project-config.html#runtime-detection
com.glassthought.sandbox.VirtualTimeDescribeSpec > Timer Service > should complete instantly with virtual time STARTED
com.glassthought.sandbox.VirtualTimeDescribeSpec > Timer Service > should complete instantly with virtual time STANDARD_OUT
[INFO][elapsed: 38ms][ā ][coroutname:@kotlinx.coroutines.test runner#5] advanceUntilIdle() auto advances
[INFO][elapsed: 42ms][ā¶][coroutname:@coroutine#6] [š¢] Delaying for [5s]
[INFO][elapsed: 43ms][ā¶][coroutname:@coroutine#6] Done delaying for [5s]
com.glassthought.sandbox.VirtualTimeDescribeSpec > Timer Service > should complete instantly with virtual time PASSED
com.glassthought.sandbox.VirtualTimeDescribeSpec > Timer Service > should control time step by step STARTED
com.glassthought.sandbox.VirtualTimeDescribeSpec > Timer Service > should control time step by step STANDARD_OUT
[INFO][elapsed: 52ms][ā·][coroutname:@kotlinx.coroutines.test runner#8] advanceTimeBy() gives precise control.
[INFO][elapsed: 52ms][āø][coroutname:@coroutine#9] [š¢] Delaying for [1s]
[INFO][elapsed: 52ms][āø][coroutname:@coroutine#9] Done delaying for [1s]
[INFO][elapsed: 52ms][āø][coroutname:@coroutine#9] [š¢] Delaying for [1s]
[INFO][elapsed: 52ms][āø][coroutname:@coroutine#9] Done delaying for [1s]
com.glassthought.sandbox.VirtualTimeDescribeSpec > Timer Service > should control time step by step PASSED
BUILD SUCCESSFUL in 2s
3 actionable tasks: 3 executed
From with-test-scope
Go to text ā
Example using virtualized time with test scope this allows us to pass the scope into our code and create test scope outside of describe spec.
Code
package com.glassthought.sandbox
import com.glassthought.sandbox.impl.CustomDescribeSpec
import com.glassthought.sandbox.util.out.impl.out
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
@OptIn(ExperimentalCoroutinesApi::class)
fun createTestScope(): TestScope = TestScope()
val sharedTestScope = createTestScope()
/** Shows how to create and pass TestScope instances for virtual time control. */
@OptIn(ExperimentalCoroutinesApi::class)
class VirtualTimeWithTestScopeDescribeSpec : CustomDescribeSpec({
describe("Timer Service with TestScope instances") {
out.delay(500.milliseconds, whatFor = "describe setup delay REAL DELAY")
it("should pass TestScope to multiple services") {
val testScope = sharedTestScope
val service1 = BackgroundService("Service-1", testScope)
val service2 = BackgroundService("Service-2", testScope)
var completedServices = 0
// Both services use same TestScope
service1.doWork {
completedServices++
out.info("Service-1 completed")
}
service2.doWork {
completedServices++
out.info("Service-2 completed")
}
out.info("Both services started, time is: ${testScope.testScheduler.currentTime}")
// Advance time in controlled steps
testScope.testScheduler.advanceTimeBy(1500.milliseconds)
testScope.testScheduler.runCurrent()
completedServices shouldBe 1 // Only Service-1 completed (1 second delay)
out.info("After 1.5s: $completedServices services completed")
testScope.testScheduler.advanceTimeBy(1000.milliseconds)
testScope.testScheduler.runCurrent()
completedServices shouldBe 2 // Both completed
out.info("After 2.5s: $completedServices services completed")
}
}
})
class BackgroundService(
private val name: String,
private val scope: CoroutineScope
) {
fun doWork(onComplete: suspend () -> Unit) {
scope.launch {
val delayTime = if (name == "Service-1") 1.seconds else 2.seconds
out.delay(delayTime, "background work for $name (VIRTUALIZED DELAY)")
onComplete()
}
}
}
Command to reproduce:
gt.sandbox.checkout.commit ba67502e427790ab7fa7 \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./gradlew test --rerun-tasks"
Recorded output of command:
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
> Task :app:checkKotlinGradlePluginConfigurationErrors SKIPPED
> Task :app:processResources NO-SOURCE
> Task :app:processTestResources NO-SOURCE
> Task :app:compileKotlin
> Task :app:compileJava NO-SOURCE
> Task :app:classes UP-TO-DATE
> Task :app:compileTestKotlin
> Task :app:compileTestJava NO-SOURCE
> Task :app:testClasses UP-TO-DATE
> Task :app:test
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug
Gradle Test Executor 3 STANDARD_OUT
Warning: Kotest autoscan is enabled. This means Kotest will scan the classpath for extensions that are annotated with @AutoScan. To avoid this startup cost, disable autoscan by setting the system property 'kotest.framework.classpath.scanning.autoscan.disable=true'. In 6.0 this value will default to true. For further details see https://kotest.io/docs/next/framework/project-config.html#runtime-detection
com.glassthought.sandbox.VirtualTimeWithTestScopeDescribeSpec > Timer Service with TestScope instances STANDARD_OUT
[INFO][elapsed: 10ms][ā ][coroutname:@coroutine#3] [š¢] Delaying for [500ms] what_for=[describe setup delay REAL DELAY]
[INFO][elapsed: 514ms][ā ][coroutname:@coroutine#3] Done delaying for [500ms] what_for=[describe setup delay REAL DELAY]
com.glassthought.sandbox.VirtualTimeWithTestScopeDescribeSpec > Timer Service with TestScope instances > should pass TestScope to multiple services STARTED
com.glassthought.sandbox.VirtualTimeWithTestScopeDescribeSpec > Timer Service with TestScope instances > should pass TestScope to multiple services STANDARD_OUT
[INFO][elapsed: 539ms][ā ][coroutname:@coroutine#3] Both services started, time is: 0
[INFO][elapsed: 539ms][ā¶][coroutname:@coroutine#4] [š¢] Delaying for [1s] what_for=[background work for Service-1 (VIRTUALIZED DELAY)]
[INFO][elapsed: 539ms][ā·][coroutname:@coroutine#5] [š¢] Delaying for [2s] what_for=[background work for Service-2 (VIRTUALIZED DELAY)]
[INFO][elapsed: 540ms][ā¶][coroutname:@coroutine#4] Done delaying for [1s] what_for=[background work for Service-1 (VIRTUALIZED DELAY)]
[INFO][elapsed: 540ms][ā¶][coroutname:@coroutine#4] Service-1 completed
[INFO][elapsed: 541ms][ā ][coroutname:@coroutine#3] After 1.5s: 1 services completed
[INFO][elapsed: 541ms][ā·][coroutname:@coroutine#5] Done delaying for [2s] what_for=[background work for Service-2 (VIRTUALIZED DELAY)]
[INFO][elapsed: 541ms][ā·][coroutname:@coroutine#5] Service-2 completed
[INFO][elapsed: 542ms][ā ][coroutname:@coroutine#3] After 2.5s: 2 services completed
com.glassthought.sandbox.VirtualTimeWithTestScopeDescribeSpec > Timer Service with TestScope instances > should pass TestScope to multiple services PASSED
BUILD SUCCESSFUL in 3s
3 actionable tasks: 3 executed
Relationships
- Respected By:withTimeout()
-
The function
withTimeout
is especially useful for testing. It can be used to test if some function takes more or less than some time. If it is used inside runTest, it will operate in virtual time. We also use it inside runBlocking to just limit the execution time of some function (this is then like setting timeout on@Test
). - Kotlin Coroutines Deep Dive
-
For Searchability
For Searchability
control fake virtualized virtual time delay
Children