Skip to content
Preptima
hardConceptCodingMidSeniorStaffLead

How do you test code that finishes later or depends on the current time, without putting a sleep in the test?

Make time and scheduling injectable. Take a java.time.Clock as a dependency so a test can fix it, and replace a fixed sleep with a bounded poll on the observable outcome, so a fast machine does not wait and a slow one does not fail.

5 min readUpdated 2026-07-29Target archetype: Big Tech, Enterprise Captive, Product Startup
Practice answering out loud

What the interviewer is scoring

  • Does the candidate make the clock a dependency instead of intercepting a static call
  • Whether waiting is expressed as a bounded poll on the outcome rather than as a fixed delay
  • That the assertion targets observable state rather than a log line or an intermediate interaction
  • Can they say what running the asynchronous path synchronously stops the test from covering
  • Whether a flaky test is treated as a defect report rather than as something to retry

Answer

Why a sleep is both too slow and too short

A Thread.sleep in a test encodes a guess about how long the work takes on the machine running it. The guess is simultaneously too large on a developer laptop, where it pads every run with idle seconds that multiply across a suite, and too small on a loaded CI agent, where the work occasionally takes longer and the test fails for reasons unrelated to the code. Both directions cost you: the first slows the feedback loop, the second erodes trust in the suite, and doubling the sleep makes the first problem worse without fixing the second.

The deeper objection is that a sleep tests nothing about timing anyway. It asserts that the outcome had appeared by an arbitrary deadline, which is neither the requirement nor a property anybody stated. What you want to assert is that the outcome appears, and separately that it appears within a bound you are willing to state.

Make the clock a dependency

Time-dependent logic becomes ordinary logic as soon as the current instant arrives from outside. java.time.Clock exists for this: it is an abstraction over "now", with Clock.systemUTC for production and Clock.fixed and Clock.offset for tests, and every now factory in java.time accepts one.

class SubscriptionService {
    private final Clock clock;   // injected, systemUTC in production

    boolean isExpired(Subscription s) {
        // Never call Instant.now with no argument in code you want to test.
        return s.expiresAt().isBefore(Instant.now(clock));
    }
}

// The test states the instant it cares about, with no waiting anywhere.
var atRenewal = Clock.fixed(Instant.parse("2026-03-01T00:00:00Z"), ZoneOffset.UTC);

The alternative people reach for is a mocking library that intercepts the static Instant.now, and it is worth being able to say why that is second best. It makes the test pass while leaving the design untestable by any other means, it is process-global so it interacts badly with parallel test execution, and it hides the dependency instead of declaring it.

Two related traps follow from the same rule. A test that computes its expected value from Instant.now() and compares it with a value the code computed from Instant.now() fails whenever the two calls straddle a boundary. And a test that fixes the clock but leaves the time zone implicit passes in one office and fails in another, so name the zone every time.

Wait for the outcome, not for a duration

Where the work genuinely happens on another thread, replace the sleep with a bounded poll: repeatedly check the condition at a short interval and fail if it has not become true within a stated timeout. That is fast when the work is fast, tolerant when the machine is slow, and it makes the deadline explicit as a documented bound rather than a magic number in the middle of the test. Awaitility is the usual library for expressing this on the JVM, and hand-rolling the loop is a dozen lines if you would rather not add a dependency.

The condition matters as much as the waiting. Poll for the outcome the caller cares about — the row is present, the message is on the queue, the balance changed — rather than for an intermediate signal such as a log line or a mock having been called. Intermediate signals can appear before the work is durable, giving you a test that passes and a system that does not work.

Where you own the callback, a CountDownLatch is better than polling: the test blocks until the code counts it down, so it finishes at the instant the work does with no interval to tune. It needs a hook, though, so it fits code you can pass a listener to rather than code whose effect you can only observe from outside.

Take the asynchrony out where the asynchrony is not the point

Most tests of a component that happens to dispatch work asynchronously are not testing the dispatch. For those, the simplest determinism is to substitute an executor that runs the task on the calling thread, so the whole path becomes synchronous and the assertion follows the call with nothing to wait for. Spring ships a synchronous task executor for exactly this substitution, and it takes one line of test configuration.

Be explicit with yourself about what that stops covering, because this is where the trade-off lives. Running on the caller's thread means the test can no longer detect that the task depended on something thread-confined, such as a security context or a request-scoped bean that will not be present on a pool thread in production. So keep one or two tests that exercise the real executor and poll for the outcome, and let the rest run synchronously. That mix is much faster than making every test wait, and it still covers the case the synchronous version cannot see.

Treat a flake as a defect

A test that passes on retry has told you something true: two orderings exist, and you have observed both. Sometimes the test is at fault, and sometimes the code has a race that will surface in production at a less convenient moment, so the first job is deciding which. @RepeatedTest in JUnit 5 run under load is a cheap way to make an intermittent failure frequent enough to diagnose, and JUnit's @Timeout is worth applying broadly so that a hung test fails with a report instead of stalling the build until the runner is killed.

What is not worth doing is adding a global retry rule to the build. It converts a signal you could have acted on into a slightly slower green build, and the race it reported is still there. Quarantining one flaky test with a linked ticket is defensible; retrying everything by default is how a suite stops meaning anything.

Inject the clock and poll for the outcome. A test that sleeps is asserting a deadline nobody specified, and it is the one test in the suite that gets slower and less reliable at the same time.

Likely follow-ups

  • What would you assert to prove a scheduled job behaves correctly at midnight in another time zone?
  • Where does a latch beat a polling wait, and where is it worse?
  • If a test fails only in CI, how do you narrow that down?
  • How do you test a retry with exponential backoff without waiting for the backoff?

Related questions

Further reading

junittestingflaky-testsclockasynchronous-code