with-test-scope

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

Backlinks