unit-test-scheduled-async — an installable skill for AI agents, published by giuseppe-trisciuoglio/developer-kit.
Unit Testing @Scheduled and @Async Methods
Overview
Patterns 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.
When to Use
Testing @Scheduled method logic
Testing @Async method behavior
Verifying CompletableFuture results
Testing async error handling
Testing cron expression logic without waiting for actual scheduling
Validating thread pool behavior and execution counts
Testing background task logic in isolation
Instructions
Call @Async methods directly — bypass Spring's async proxy; the annotation is irrelevant in unit tests
Mock dependencies with @Mock and @InjectMocks (Mockito)
Wait for completion — use CompletableFuture.get(timeout, unit) or await().atMost(...).untilAsserted(...)
Call @Scheduled methods directly — do not wait for cron/fixedRate; the annotation is ignored in unit tests
Test exception paths — verify ExecutionException wrapping on CompletableFuture.get()
Validation checkpoints:
After CompletableFuture.get(), assert the returned value before verifying mock interactions
If ExecutionException is thrown, check .getCause() to identify the root exception
If Awaitility times out, increase atMost() duration or reduce pollInterval() until the condition is reachable
After multiple task invocations, assert execution counts before verify() calls
Examples
Key patterns — complete examples in references/examples.md:
// @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.
Best Practices
Always set a timeout on CompletableFuture.get() to prevent hanging tests
Mock all dependencies — never call real external services in unit tests
Use Awaitility only for race conditions; prefer direct calls for simple async methods
Test @Scheduled logic directly — the annotation is ignored in unit tests
Assert values before verifying mock interactions; verify after async completion
Common Pitfalls
Relying on Spring's async executor instead of calling methods directly
Missing timeout on CompletableFuture.get()
Forgetting to test exception propagation in async methods
Not mocking dependencies that async methods invoke internally
Waiting for actual cron/fixedRate timing instead of testing logic in isolation
Constraints and Warnings
@Async self-invocation: calling @Async from another method in the same class executes synchronously — the Spring proxy is bypassed
Thread pool ordering: ThreadPoolTaskScheduler does not guarantee execution order
CompletableFuture chaining: exceptions in intermediate stages can be silently lost — test each stage
Awaitility timeout: always set a reasonable atMost(); infinite waits hang the test suite
No actual scheduling: @Scheduled is ignored in unit tests — call methods directly
References
Spring @Async Documentation
Spring @Scheduled Documentation
Awaitility Testing Library
CompletableFuture API
Code examples: references/examples.mddon't have the plugin yet? install it then click "run inline in claude" again.