
Lightweight, thread-safe test clocks offering deterministic time control for TTLs, expirations and backoffs; includes mutable and fixed clocks, coroutine scheduler bridge, and atomic concurrency safety.
Kotlin Multiplatform test doubles for kotlin.time.Clock.
Whenever you write code that deals with TTLs, token expiration, retry backoffs, or cache eviction, you need a deterministic way to control time in your tests. Ticker provides lightweight, thread-safe clock doubles designed specifically for this purpose.
val clock = MutableClock(Instant.parse("2026-01-01T00:00:00Z"))
val cache = MyCache(ttl = 5.minutes, clock = clock)
cache.put("key", "value")
clock.advanceBy(6.minutes)
assertNull(cache.get("key"))Writing a quick fake clock in a couple lines usually introduces subtle bugs:
instant += duration implementation relies on a non-atomic read-modify-write. When multiple threads or concurrent test workers advance time simultaneously, updates get silently lost. MutableClock uses an AtomicReference compare-and-set loop, so concurrent advanceBy, setTo, and now calls do not drop updates. Lost-update tests run on the JVM.runTest, calling advanceTimeBy(1.hours) advances virtual time on the test dispatcher, but leaves independent clock objects behind. This causes code checking clock.now() to see time standing still while delayed coroutines resume. The ticker-coroutines module seamlessly bridges TestCoroutineScheduler virtual time to kotlin.time.Clock.Add the dependencies to your test source set:
kotlin {
sourceSets {
commonTest.dependencies {
implementation("io.github.ivamsi:ticker:0.1.0")
implementation("io.github.ivamsi:ticker-coroutines:0.1.0") // optional
}
}
}| Class / Function | Purpose |
|---|---|
MutableClock(instant) |
Controllable clock supporting advanceBy(duration) and setTo(instant). |
FixedClock(instant) |
Immutable clock that always returns a constant instant. |
TestCoroutineScheduler.asClock(start) |
Clock derived from coroutine virtual time, in whole milliseconds (ticker-coroutines). |
Tip:
advanceByonly accepts non-negative durations because stepping backward during an "advance" is almost always a test logic error. To jump backward or simulate time synchronization, usesetTo(instant).
asClock reads TestCoroutineScheduler.currentTime on every now(). Virtual time is whole milliseconds, so a sub-millisecond delay does not move the clock until a full millisecond elapses. advanceTimeBy and delay still keep clock time and virtual time in step:
@Test
fun `session expires after timeout`() = runTest {
val clock = testScheduler.asClock(start = Instant.parse("2026-01-01T00:00:00Z"))
val sessionManager = SessionManager(timeout = 15.minutes, clock = clock)
sessionManager.login("user_123")
advanceTimeBy(16.minutes)
assertFalse(sessionManager.isSessionActive("user_123"))
}Ticker supports all major Kotlin Multiplatform targets:
Running iOS simulator tests requires an installed runtime and a device configured for your Xcode SDK. If Kotlin reports:
"Xcode does not support simulator tests for ios_simulator_arm64. Check that requested SDK is installed."
You can verify your local simulator setup:
xcrun simctl list runtimes # check if the required iOS runtime is installed
xcrun simctl list devices available # check if an active device exists for that runtimeIf the runtime is missing, download it via xcodebuild -downloadPlatform iOS. If the runtime is installed but no devices exist, create one:
xcrun simctl create "iPhone 16" \
com.apple.CoreSimulator.SimDeviceType.iPhone-16 \
com.apple.CoreSimulator.SimRuntime.iOS-26-5:ticker depends solely on the Kotlin standard library. :ticker-coroutines adds only kotlinx-coroutines-test.MutableClock uses Kotlin's kotlin.concurrent.atomics internally without leaking experimental opt-ins into your consumer code../gradlew checkLegacyAbi so a public-API change has to update the dump files.Copyright 2026 Vamsi Vaddavalli
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Kotlin Multiplatform test doubles for kotlin.time.Clock.
Whenever you write code that deals with TTLs, token expiration, retry backoffs, or cache eviction, you need a deterministic way to control time in your tests. Ticker provides lightweight, thread-safe clock doubles designed specifically for this purpose.
val clock = MutableClock(Instant.parse("2026-01-01T00:00:00Z"))
val cache = MyCache(ttl = 5.minutes, clock = clock)
cache.put("key", "value")
clock.advanceBy(6.minutes)
assertNull(cache.get("key"))Writing a quick fake clock in a couple lines usually introduces subtle bugs:
instant += duration implementation relies on a non-atomic read-modify-write. When multiple threads or concurrent test workers advance time simultaneously, updates get silently lost. MutableClock uses an AtomicReference compare-and-set loop, so concurrent advanceBy, setTo, and now calls do not drop updates. Lost-update tests run on the JVM.runTest, calling advanceTimeBy(1.hours) advances virtual time on the test dispatcher, but leaves independent clock objects behind. This causes code checking clock.now() to see time standing still while delayed coroutines resume. The ticker-coroutines module seamlessly bridges TestCoroutineScheduler virtual time to kotlin.time.Clock.Add the dependencies to your test source set:
kotlin {
sourceSets {
commonTest.dependencies {
implementation("io.github.ivamsi:ticker:0.1.0")
implementation("io.github.ivamsi:ticker-coroutines:0.1.0") // optional
}
}
}| Class / Function | Purpose |
|---|---|
MutableClock(instant) |
Controllable clock supporting advanceBy(duration) and setTo(instant). |
FixedClock(instant) |
Immutable clock that always returns a constant instant. |
TestCoroutineScheduler.asClock(start) |
Clock derived from coroutine virtual time, in whole milliseconds (ticker-coroutines). |
Tip:
advanceByonly accepts non-negative durations because stepping backward during an "advance" is almost always a test logic error. To jump backward or simulate time synchronization, usesetTo(instant).
asClock reads TestCoroutineScheduler.currentTime on every now(). Virtual time is whole milliseconds, so a sub-millisecond delay does not move the clock until a full millisecond elapses. advanceTimeBy and delay still keep clock time and virtual time in step:
@Test
fun `session expires after timeout`() = runTest {
val clock = testScheduler.asClock(start = Instant.parse("2026-01-01T00:00:00Z"))
val sessionManager = SessionManager(timeout = 15.minutes, clock = clock)
sessionManager.login("user_123")
advanceTimeBy(16.minutes)
assertFalse(sessionManager.isSessionActive("user_123"))
}Ticker supports all major Kotlin Multiplatform targets:
Running iOS simulator tests requires an installed runtime and a device configured for your Xcode SDK. If Kotlin reports:
"Xcode does not support simulator tests for ios_simulator_arm64. Check that requested SDK is installed."
You can verify your local simulator setup:
xcrun simctl list runtimes # check if the required iOS runtime is installed
xcrun simctl list devices available # check if an active device exists for that runtimeIf the runtime is missing, download it via xcodebuild -downloadPlatform iOS. If the runtime is installed but no devices exist, create one:
xcrun simctl create "iPhone 16" \
com.apple.CoreSimulator.SimDeviceType.iPhone-16 \
com.apple.CoreSimulator.SimRuntime.iOS-26-5:ticker depends solely on the Kotlin standard library. :ticker-coroutines adds only kotlinx-coroutines-test.MutableClock uses Kotlin's kotlin.concurrent.atomics internally without leaking experimental opt-ins into your consumer code../gradlew checkLegacyAbi so a public-API change has to update the dump files.Copyright 2026 Vamsi Vaddavalli
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0