skip to content

How do you test time-dependent code deterministically with TestTimeSource? Show how marks behave as you advance virtual time.

level: seniorimportance: should knowfreq 35%

answer

  1. TestTimeSource = manually-advanced fake clock
  2. Advance with += Duration (plusAssign)
  3. Never advances on its own; starts at zero
  4. Negative/overflow advance -> IllegalStateException
  5. Inject TimeSource param; default Monotonic

basics

~20 s

TestTimeSource is a fake clock you control. It does not advance on its own; you call plusAssignDuration (the += operator) to move virtual time forward. Marks taken from it then report exactly the elapsed time you advanced, so tests are deterministic.

solid answer

~30 s

kotlin.time.TestTimeSource is a TimeSource.WithComparableMarks whose reading only changes when you advance it manually with the += operator (operator fun plusAssign(duration: Duration)). Inject it where production code would use TimeSource.Monotonic. In the test, call markNow(), advance virtual time with source += 5.seconds, then assert mark.elapsedNow() == 5.seconds — no real waiting, no flakiness, nanosecond-exact control. Marks from a TestTimeSource are comparable and support the same arithmetic as Monotonic marks. Advancing by a negative or overflowing amount throws IllegalStateException to prevent nonsensical backward time. This is the standard way to unit-test caches with TTLs, retry/backoff, rate limiters, and timeouts without sleeping.

code

kotlin · 21 lines
kotlin
import kotlin.time.TestTimeSource
import kotlin.time.TimeSource
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

class RateLimiter(private val window: Duration, private val source: TimeSource = TimeSource.Monotonic) {
    private var windowStart = source.markNow()
    private var count = 0
    fun allow(max: Int): Boolean {
        if (windowStart.elapsedNow() >= window) { windowStart = source.markNow(); count = 0 }
        return if (count < max) { count++; true } else false
    }
}

fun test() {
    val ts = TestTimeSource()
    val rl = RateLimiter(1.seconds, ts)
    check(rl.allow(1)); check(!rl.allow(1)) // limited within window
    ts += 1.seconds
    check(rl.allow(1))                       // new window opened
}

go deeper

for a junior

Knows TestTimeSource is a fake clock used in tests.

for a middle

Can advance it with += and assert elapsedNow() reflects the advance.

for a senior

Designs production code with an injected TimeSource defaulting to Monotonic and writes deterministic tests; knows negative advance throws.

for a principal

Distinguishes TestTimeSource from coroutine virtual time, sets a testability convention (inject the clock) across the codebase, and reasons about overflow/edge guarantees.

## Why fake time Testing code that 'expires after 5 seconds' by actually sleeping 5 seconds is slow and flaky. **`TestTimeSource`** lets you control the clock so tests are instant and deterministic. ## What it is `kotlin.time.TestTimeSource` is a concrete `TimeSource.WithComparableMarks`. Its current reading starts at zero and **never advances on its own** — only when *you* push it forward. ## Advancing virtual time You move it forward with the **`+=` operator** (`operator fun plusAssign(duration: Duration)`): ```kotlin import kotlin.time.TestTimeSource import kotlin.time.Duration.Companion.seconds val ts = TestTimeSource() val mark = ts.markNow() println(mark.elapsedNow()) // 0s ts += 5.seconds // advance virtual time println(mark.elapsedNow()) // 5s — exactly ``` Advancing by a negative duration, or by an amount that would overflow the internal counter, throws **`IllegalStateException`** — virtual time cannot go backward or overflow silently. ## Dependency injection is the key Production code must not hardcode `TimeSource.Monotonic`. Accept a `TimeSource` parameter (defaulting to `Monotonic`) so tests can pass a `TestTimeSource`: ```kotlin import kotlin.time.TimeSource import kotlin.time.Duration class TtlCache<K, V>( private val ttl: Duration, private val source: TimeSource = TimeSource.Monotonic, ) { private data class Entry<V>(val value: V, val storedAt: TimeSource.Monotonic.ValueTimeMark) // ... store source.markNow(); expired when storedAt.elapsedNow() >= ttl } ``` Tip: type the source as the interface `TimeSource` (or `TimeSource.WithComparableMarks` if you need comparison) so both `Monotonic` and `TestTimeSource` fit. ## A full test ```kotlin import kotlin.test.Test import kotlin.test.assertEquals import kotlin.time.TestTimeSource import kotlin.time.Duration.Companion.seconds class TimingTest { @Test fun elapsedTracksVirtualTime() { val ts = TestTimeSource() val mark = ts.markNow() ts += 3.seconds assertEquals(3.seconds, mark.elapsedNow()) ts += 2.seconds assertEquals(5.seconds, mark.elapsedNow()) } } ``` ## Relationship to coroutines test time `TestTimeSource` is the *low-level* primitive for any timing code. For suspending code, `kotlinx-coroutines-test`'s `runTest`/`TestCoroutineScheduler` provides its own virtual clock for delays — separate machinery, same idea. Don't confuse the two: `TestTimeSource` controls a `TimeSource`, the scheduler controls coroutine `delay`.

  • Does TestTimeSource advance automatically when wall-clock time passes during the test?
    No. It only moves when you call += with a Duration. Real time passing has zero effect on it, which is exactly what makes tests deterministic.
  • What happens if you advance a TestTimeSource by a negative Duration?
    It throws IllegalStateException. Virtual time is not allowed to move backward (and it also guards against counter overflow).
  • How is this different from kotlinx-coroutines-test virtual time?
    TestTimeSource controls a TimeSource for elapsed measurement; the coroutines TestCoroutineScheduler controls delay() in suspending code. Different APIs, same deterministic-time philosophy.

A film director controlling a stopwatch by hand on set: time only moves when the director says 'advance five seconds,' so every take is identical.

saying these in an interview costs you the question

  • Hardcoding TimeSource.Monotonic so tests cannot inject a fake
  • Expecting TestTimeSource to advance as real time passes
  • Calling Thread.sleep in tests instead of advancing virtual time
  • Trying to move TestTimeSource backward with a negative duration
  • Confusing TestTimeSource with the coroutines test scheduler

context