Testland
Browse all skills & agents

mockclock-jvm

Wraps Java's java.time.Clock + InstantSource dependency-injection pattern for testing time-sensitive code. Covers Clock.fixed(instant, zone), Clock.offset(baseClock, duration), Clock.systemDefaultZone() for production, the InstantSource interface (Java 17+), and the recommended dependency-injection pattern (constructor-inject Clock instead of calling Instant.now() directly). Use when you need to wire the clock-injection pattern (Clock.fixed, Clock.offset, MutableClock, InstantSource, Spring @Bean) into JVM (Java / Kotlin / Scala) production or test code. For pure DST transition reference (skipped or repeated hours, IANA DB, cron-double-fire bug classes) without a clock-injection need, use dst-transition-reference instead. Composes dst-transition-reference + iso-8601-vs-rfc-3339-reference.

mockclock-jvm

Overview

The JVM doesn't have a single "freeze clock" library because java.time (since Java 8) was designed with dependency-injected Clock as the intended testing pattern. Per docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/Clock.html: "This abstract class models a clock. Most application code should inject a Clock into any method that needs the current instant and date/time."

The pattern: production code injects Clock.systemDefaultZone(); tests inject Clock.fixed(...). No global state monkey-patching.

When to use

  • Java / Kotlin / Scala tests for time-sensitive code.
  • Designing testable code from the start (constructor-inject Clock).
  • Migrating away from System.currentTimeMillis() and Instant.now().

Authoring

The injection pattern

// Production code
public class TaskScheduler {
    private final Clock clock;

    public TaskScheduler(Clock clock) {
        this.clock = clock;
    }

    public Task scheduleNext(Duration after) {
        return new Task(Instant.now(clock).plus(after));
    }
}

// Wire-up
TaskScheduler prodScheduler = new TaskScheduler(Clock.systemDefaultZone());

The injection lets tests pass any Clock implementation.

Clock.fixed (frozen)

@Test
void scheduleNext() {
    Clock fixed = Clock.fixed(
        Instant.parse("2026-05-20T14:30:00Z"),
        ZoneId.of("America/New_York")
    );
    TaskScheduler scheduler = new TaskScheduler(fixed);

    Task task = scheduler.scheduleNext(Duration.ofMinutes(5));

    assertEquals(
        Instant.parse("2026-05-20T14:35:00Z"),
        task.getScheduledAt()
    );
}

Clock.fixed doesn't advance. Successive Instant.now(fixed) returns the same value.

Clock.offset (relative)

Clock realPlus10 = Clock.offset(Clock.systemDefaultZone(), Duration.ofMinutes(10));
// Code reading this clock sees real-time + 10 minutes

Useful when you want real-time progression but with a known offset.

A mutable test clock (Java 17+)

// Custom: a Clock that can be advanced manually
public class MutableClock extends Clock {
    private Instant instant;
    private final ZoneId zone;

    public MutableClock(Instant instant, ZoneId zone) {
        this.instant = instant;
        this.zone = zone;
    }

    public void setInstant(Instant i) { this.instant = i; }
    public void advance(Duration d) { instant = instant.plus(d); }

    @Override public Clock withZone(ZoneId z) { return new MutableClock(instant, z); }
    @Override public ZoneId getZone() { return zone; }
    @Override public Instant instant() { return instant; }
}

Tests can setInstant / advance to simulate time progression.

InstantSource (Java 17+)

Per docs.oracle.com, InstantSource is a narrower interface than Clock (just instant() - no zone). Useful when code only needs the instant:

public class Logger {
    private final InstantSource source;
    public Logger(InstantSource source) { this.source = source; }
    public void log(String msg) {
        System.out.println(source.instant() + " " + msg);
    }
}

// Test
InstantSource fake = () -> Instant.parse("2026-05-20T14:30:00Z");
new Logger(fake).log("hi");

Spring DI integration

@Configuration
public class ClockConfig {
    @Bean
    public Clock clock() {
        return Clock.systemDefaultZone();
    }
}

// Test
@TestConfiguration
public class TestClockConfig {
    @Bean public Clock clock() {
        return Clock.fixed(Instant.parse("2026-05-20T14:30:00Z"), ZoneOffset.UTC);
    }
}

DST tests

@Test
void springForwardLocalTime() {
    Clock fixed = Clock.fixed(
        Instant.parse("2026-03-08T07:30:00Z"),  // 02:30 EDT — invalid local
        ZoneId.of("America/New_York")
    );

    // ZonedDateTime resolution depends on resolver
    ZonedDateTime zdt = ZonedDateTime.ofInstant(fixed.instant(), fixed.getZone());
    // Per dst-transition-reference: assert the behaviour matches docs
}

Running

mvn test
gradle test

CI integration

jobs:
  jvm-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-java@v4
        with: { java-version: '21', distribution: 'temurin' }
      - run: mvn test

Anti-patterns

Anti-patternWhy it failsFix
Instant.now() directly in codeNot injectable; not testableInject Clock; Instant.now(clock)
System.currentTimeMillis()Same problemInject Clock
Static-mock the Clock with PowerMockBrittle; bytecode-rewrite hacksUse DI
Forget zone in Clock.fixedDefault UTC; local-time tests degenerateExplicit zone
Test only frozen, not advancingMisses duration arithmeticCustom MutableClock
Skip InstantSource for narrow needsOver-specified Clock dependencyUse InstantSource (Java 17+)
Live-clock in unit testsFlaky around minute / hour boundariesAlways inject test Clock
Multiple Clocks per serviceCoordination bugsOne Clock per service

Limitations

  • Requires source control. Libraries that call Instant.now() directly aren't testable via Clock injection.
  • System.currentTimeMillis() not covered. Use Clock everywhere.
  • Thread.sleep is real-time. Mockclock pattern doesn't fake sleep; use a ScheduledExecutorService with a controllable executor for that.
  • No leap-second simulation per leap-second-reference.

References