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:
- Lost updates under concurrency: A naive
instant += durationimplementation relies on a non-atomic read-modify-write. When multiple threads or concurrent test workers advance time simultaneously, updates get silently lost.MutableClockuses anAtomicReferencecompare-and-set loop, so concurrentadvanceBy,setTo, andnowcalls do not drop updates. Lost-update tests run on the JVM. - Desynchronization in coroutine tests: When using
runTest, callingadvanceTimeBy(1.hours)advances virtual time on the test dispatcher, but leaves independent clock objects behind. This causes code checkingclock.now()to see time standing still while delayed coroutines resume. Theticker-coroutinesmodule seamlessly bridgesTestCoroutineSchedulervirtual time tokotlin.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:
- JVM / Android (Android projects consume the JVM artifact)
- JavaScript (Node.js)
- WebAssembly (Wasm/JS Node.js)
- Apple: iOS (x64, arm64, simulator arm64), macOS (arm64)
- Linux: Linux x64
- Windows: MinGW x64
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- Zero extra dependencies:
:tickerdepends solely on the Kotlin standard library.:ticker-coroutinesadds onlykotlinx-coroutines-test. - Internal Atomics:
MutableClockuses Kotlin'skotlin.concurrent.atomicsinternally without leaking experimental opt-ins into your consumer code. - Strict ABI Validation: Both artifacts enforce Kotlin Explicit API mode and ABI dumps. CI runs
./gradlew checkLegacyAbiso 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