Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Provides patterns for unit testing Spring `@Scheduled` and `@Async` methods using JUnit 5, CompletableFuture, Awaitility, and Mockito. Covers mocking task execution and timing, verifying execution counts, testing cron expressions, validating retry behavior, and simulating thread pool behavior. Use when testing background tasks, cron jobs, periodic execution, scheduled tasks, or thread pool behavior.
.claude/skills/giuseppe-trisciuoglio-unit-test-scheduled-async/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 52% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 21% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 22% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 36% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 34% | 0% |
@Scheduled and @Async MethodsPatterns for unit testing Spring @Scheduled and @Async methods with JUnit 5. Test CompletableFuture results, use Awaitility for race conditions, mock scheduled task execution, and validate error handling — without waiting for real scheduling intervals.
@Scheduled method logic@Async method behaviorCompletableFuture results@Async methods directly — bypass Spring's async proxy; the annotation is irrelevant in unit tests@Mock and @InjectMocks (Mockito)CompletableFuture.get(timeout, unit) or await().atMost(...).untilAsserted(...)@Scheduled methods directly — do not wait for cron/fixedRate; the annotation is ignored in unit testsExecutionException wrapping on CompletableFuture.get()Validation checkpoints:
CompletableFuture.get(), assert the returned value before verifying mock interactionsExecutionException is thrown, check .getCause() to identify the root exceptionatMost() duration or reduce pollInterval() until the condition is reachableverify() callsKey patterns — complete examples in references/examples.md:
java// @Async: call directly, wait with CompletableFuture.get(timeout, unit) @Service class EmailService { @Async public CompletableFuture<Boolean> sendEmailAsync(String to) { return CompletableFuture.supplyAsync(() -> true); } } @Test void shouldReturnCompletedFuture() throws Exception { EmailService service = new EmailService(); Boolean result = service.sendEmailAsync("test@example.com").get(5, TimeUnit.SECONDS); assertThat(result).isTrue(); } // @Scheduled: call directly, mock the repository @Component class DataRefreshTask { @InjectMocks private DataRepository dataRepository; @Scheduled(fixedDelay = 60000) public void refreshCache() { /* ... */ } } @Test void shouldRefreshCache() { when(dataRepository.findAll()).thenReturn(List.of(new Data(1L, "item1"))); dataRefreshTask.refreshCache(); verify(dataRepository).findAll(); } // Awaitility: use for race conditions with shared mutable state @Test void shouldProcessAllItems() { BackgroundWorker worker = new BackgroundWorker(); worker.processItems(List.of("item1", "item2", "item3")); Awaitility.await() .atMost(Duration.ofSeconds(5)) .pollInterval(Duration.ofMillis(100)) .untilAsserted(() -> assertThat(worker.getProcessedCount()).isEqualTo(3)); } // Mocked dependencies with exception handling @Test void shouldHandleAsyncExceptionGracefully() { doThrow(new RuntimeException("Email failed")).when(emailService).send(any()); CompletableFuture<String> result = service.notifyUserAsync("user123"); assertThatThrownBy(result::get) .isInstanceOf(ExecutionException.class) .hasCauseInstanceOf(RuntimeException.class); }
Full Maven/Gradle dependencies, additional test classes, and execution count patterns: see references/examples.md.
CompletableFuture.get() to prevent hanging tests@Scheduled logic directly — the annotation is ignored in unit testsCompletableFuture.get()@Async self-invocation: calling @Async from another method in the same class executes synchronously — the Spring proxy is bypassedThreadPoolTaskScheduler does not guarantee execution orderatMost(); infinite waits hang the test suite@Scheduled is ignored in unit tests — call methods directly@Async Documentation@Scheduled Documentationreferences/examples.md| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 12,160 | 11,148 | -8% | 1 | 1 | 0% | 2,322 | 3,537 | +52% | 0 | 0 | — |
case-02 | fail→pass | 11,808 | 7,084 | -40% | 1 | 1 | 0% | 2,287 | 2,767 | +21% | 0 | 0 | — |
case-03 | pass→pass | 13,164 | 7,175 | -45% | 1 | 1 | 0% | 2,329 | 2,768 | +19% | 0 | 0 | — |
case-04 | pass→pass | 10,175 | 8,299 | -18% | 1 | 1 | 0% | 1,891 | 2,659 | +41% | 0 | 0 | — |
case-05 | fail→pass | 10,601 | 6,226 | -41% | 1 | 1 | 0% | 2,100 | 2,555 | +22% | 0 | 0 | — |
case-06 | fail→pass | 11,443 | 12,893 | +13% | 1 | 1 | 0% | 2,078 | 2,836 | +36% | 0 | 0 | — |
case-07 | pass→pass | 13,100 | 16,901 | +29% | 1 | 1 | 0% | 2,280 | 3,139 | +38% | 0 | 0 | — |
case-08 | pass→pass | 11,974 | 6,370 | -47% | 1 | 1 | 0% | 1,878 | 2,212 | +18% | 0 | 0 | — |
case-09 | pass→pass | 5,669 | 3,549 | -37% | 1 | 1 | 0% | 805 | 1,844 | +129% | 0 | 0 | — |
case-10 | fail→pass | 11,331 | 7,078 | -38% | 1 | 1 | 0% | 1,795 | 2,397 | +34% | 0 | 0 | — |
case-15 | pass→pass | 11,783 | 7,986 | -32% | 1 | 1 | 0% | 2,357 | 2,681 | +14% | 0 | 0 | — |
case-11 | pass→pass | 8,770 | 5,681 | -35% | 1 | 1 | 0% | 1,358 | 2,061 | +52% | 0 | 0 | — |
case-12 | pass→pass | 16,846 | 13,277 | -21% | 1 | 1 | 0% | 2,833 | 3,629 | +28% | 0 | 0 | — |
case-13 | pass→pass | 9,928 | 9,003 | -9% | 1 | 1 | 0% | 1,899 | 3,020 | +59% | 0 | 0 | — |
case-14 | pass→pass | 13,883 | 9,283 | -33% | 1 | 1 | 0% | 2,616 | 3,108 | +19% | 0 | 0 | — |
case-16 | pass→pass | 12,651 | 8,746 | -31% | 1 | 1 | 0% | 2,067 | 2,644 | +28% | 0 | 0 | — |
case-17 | pass→pass | 10,994 | 6,647 | -40% | 1 | 1 | 0% | 1,858 | 2,365 | +27% | 0 | 0 | — |
case-18 | pass→pass | 9,205 | 6,187 | -33% | 1 | 1 | 0% | 1,822 | 2,366 | +30% | 0 | 0 | — |
case-19 | fail→pass | 8,686 | 6,432 | -26% | 1 | 1 | 0% | 1,449 | 2,442 | +69% | 0 | 0 | — |
case-20 | pass→pass | 6,808 | 4,051 | -40% | 1 | 1 | 0% | 1,201 | 1,957 | +63% | 0 | 0 | — |
case-21 | fail→pass | 12,024 | 8,222 | -32% | 1 | 1 | 0% | 2,204 | 2,787 | +26% | 0 | 0 | — |
case-22 | pass→pass | 7,648 | 4,772 | -38% | 1 | 1 | 0% | 1,279 | 1,974 | +54% | 0 | 0 | — |
case-23 | pass→pass | 15,476 | 7,965 | -49% | 1 | 1 | 0% | 2,649 | 2,681 | +1% | 0 | 0 | — |
case-24 | fail→fail | 12,182 | 9,896 | -19% | 1 | 1 | 0% | 2,363 | 3,438 | +45% | 0 | 0 | — |
case-25 | pass→pass | 12,982 | 9,398 | -28% | 1 | 1 | 0% | 2,444 | 3,082 | +26% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 25 cases were attempted. The headline lift of +28 percentage points is the difference between those two pass rates over the 25 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.