Example using runTest to control virtual time in tests.
Code
package com.glassthought.sandboximport com.glassthought.sandbox.impl.CustomDescribeSpecimport com.glassthought.sandbox.util.out.impl.outimport io.kotest.matchers.shouldBeimport kotlinx.coroutines.ExperimentalCoroutinesApiimport kotlinx.coroutines.launchimport kotlinx.coroutines.test.advanceTimeByimport kotlinx.coroutines.test.advanceUntilIdleimport kotlinx.coroutines.test.runTestimport kotlin.time.Duration.Companion.millisecondsimport 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 = 0private 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:testPicked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debugGradle 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-detectioncom.glassthought.sandbox.VirtualTimeDescribeSpec > Timer Service > should complete instantly with virtual time STARTEDcom.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 PASSEDcom.glassthought.sandbox.VirtualTimeDescribeSpec > Timer Service > should control time step by step STARTEDcom.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 PASSEDBUILD SUCCESSFUL in 2s3 actionable tasks: 3 executed
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.sandboximport com.glassthought.sandbox.impl.CustomDescribeSpecimport com.glassthought.sandbox.util.out.impl.outimport io.kotest.matchers.shouldBeimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.ExperimentalCoroutinesApiimport kotlinx.coroutines.delayimport kotlinx.coroutines.launchimport kotlinx.coroutines.test.*import kotlin.time.Duration.Companion.millisecondsimport 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:testPicked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debugGradle 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-detectioncom.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 STARTEDcom.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 completedcom.glassthought.sandbox.VirtualTimeWithTestScopeDescribeSpec > Timer Service with TestScope instances > should pass TestScope to multiple services PASSEDBUILD SUCCESSFUL in 3s3 actionable tasks: 3 executed
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