Test a due-time rule without waiting for a real scheduler tick or relying on wall-clock races.
Spring scheduled worker test: inject a clock and call the work once
Separate trigger from work
@Scheduled decides when to call a method. A reminder worker decides which due rows to claim and how many to process. Put that decision in a callable service with an injected Clock; keep the scheduled adapter small. A test can advance a fixed clock and call one work cycle directly. The scheduled outbox pattern] still needs database ownership rules across replicas.
Make time a precise input
At 09:47 UTC, a reminder due exactly at 09:47 should be included if the contract says dueAt less than or equal to now. Seed one row just before, one equal, and one after the boundary. Assert a bounded batch of 47 and the resulting state. Do not use Thread.sleep or depend on the CI machine timezone. Test shutdown and draining] separately because a fixed clock does not exercise scheduler lifecycle.
Keep concurrency in another test
A direct call proves selection logic, not that two application replicas cannot claim the same row. Use an integration test with two independent workers and a real database lock or claim update for that invariant; poller integration tests] cover it. Capture failures as retryable state rather than letting a scheduled method's exception lose a work item.
Implementation contract
final class ReminderWorker {
private final Clock clock;
private final ReminderStore reminders;
ReminderWorker(Clock clock, ReminderStore reminders) {
this.clock = clock;
this.reminders = reminders;
}
int runOneCycle() {
return reminders.claimDueBeforeOrAt(clock.instant(), 47).size();
}
}
class ReminderWorkerClockTest {
@Test void usesFixedCutoff() {
Instant cutoff = Instant.parse("2026-10-05T09:47:00Z");
ReminderStore store = mock(ReminderStore.class);
when(store.claimDueBeforeOrAt(cutoff, 47)).thenReturn(List.of());
Clock fixed = Clock.fixed(cutoff, ZoneOffset.UTC);
assertThat(new ReminderWorker(fixed, store).runOneCycle()).isZero();
verify(store).claimDueBeforeOrAt(cutoff, 47);
}
}Cost and verification
A fixed clock removes waiting and makes boundary tests fast. It does not replace a database concurrency test or a scheduler lifecycle test; those cost more because they exercise real threads and storage ownership.
Common Mistakes
- Do not sleep until a scheduled tick in a unit test.
- Do not let system timezone decide a UTC due-time assertion.
- Do not mistake a single-worker fixed-clock test for multi-replica claim safety.
Read next
Spring TaskScheduler: poll committed outbox rows in the background, Spring relay integration tests: prove the boundaries H2 cannot, Spring TaskScheduler shutdown: stop new polls and account for in-flight work, Spring outbox polling: cap work per tick and measure backlog age, Test a Spring worker at admission, lease and replay boundaries.
