
Unit Test Scheduled Async
- 1.6k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
Patterns and practices for unit testing Spring @Scheduled and @Async methods using JUnit 5, CompletableFuture, Awaitility, and Mockito.
About
Provides JUnit 5 patterns for testing Spring @Scheduled and @Async methods using CompletableFuture, Awaitility, and Mockito. Developers use this when testing background tasks, cron jobs, and async error handling in isolation. Key workflows include calling @Async/@Scheduled methods directly (bypassing Spring proxies), mocking dependencies with Mockito, waiting for completion with CompletableFuture.get() or Awaitility, and validating execution counts and exception propagation. Covers race conditions, timeout management, and testing CompletableFuture result values before verifying mock interactions.
- Call @Async and @Scheduled methods directly in tests; Spring annotations are ignored in unit tests
- Use CompletableFuture.get(timeout, unit) with timeout to prevent test hangs
- Employ Awaitility with atMost() and pollInterval() for race conditions and shared mutable state
- Mock all dependencies with @Mock and @InjectMocks; test exception propagation via ExecutionException.getCause()
- Verify execution counts and mock interactions only after async completion and value assertions
Unit Test Scheduled Async by the numbers
- 1,635 all-time installs (skills.sh)
- +55 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #443 of 2,184 Testing & QA skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
unit-test-scheduled-async capabilities & compatibility
- Capabilities
- direct method invocation bypassing spring async · timeout safe waiting with completablefuture.get( · race condition handling with awaitility polling · mock dependency setup and verification · exception propagation and executionexception unw · execution count validation for repeated task inv
- Use cases
- testing · debugging
- Platforms
- macOS · Windows · Linux
- IDEs
- vscode · intellij · jetbrains
- Runs
- Runs locally
- Pricing
- Free
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-scheduled-asyncAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.6k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
What it does
Unit test Spring @Scheduled and @Async methods with JUnit 5, mocking, and Awaitility without waiting for real scheduling intervals.
Who is it for?
Backend developers testing Spring scheduled and async task logic, cron expression validation, thread pool behavior, and async error handling.
Skip if: Integration testing actual Spring task scheduling, end-to-end async workflows with real external services, or timing-sensitive performance benchmarks.
When should I use this skill?
Writing unit tests for Spring @Scheduled methods, @Async methods returning CompletableFuture, background task logic, or async exception handling.
What you get
Developers can confidently unit test @Scheduled cron jobs, @Async background tasks, and CompletableFuture chains with proper mocking, timeout management, and exception validation.
- Unit test patterns for @Scheduled and @Async methods
- CompletableFuture timeout and exception handling examples
- Awaitility usage patterns for race conditions
By the numbers
- Covers timeout management with CompletableFuture.get(timeout, unit)
- Includes Awaitility polling patterns with atMost() and pollInterval() durations
- Provides exception handling via ExecutionException.getCause() validation
Files
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
@Scheduledmethod logic - Testing
@Asyncmethod behavior - Verifying
CompletableFutureresults - 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
1. Call `@Async` methods directly — bypass Spring's async proxy; the annotation is irrelevant in unit tests 2. Mock dependencies with @Mock and @InjectMocks (Mockito) 3. Wait for completion — use CompletableFuture.get(timeout, unit) or await().atMost(...).untilAsserted(...) 4. Call `@Scheduled` methods directly — do not wait for cron/fixedRate; the annotation is ignored in unit tests 5. Test exception paths — verify ExecutionException wrapping on CompletableFuture.get()
Validation checkpoints:
- After
CompletableFuture.get(), assert the returned value before verifying mock interactions - If
ExecutionExceptionis thrown, check.getCause()to identify the root exception - If Awaitility times out, increase
atMost()duration or reducepollInterval()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
@Scheduledlogic 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
@Asyncfrom another method in the same class executes synchronously — the Spring proxy is bypassed - Thread pool ordering:
ThreadPoolTaskSchedulerdoes 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:
@Scheduledis ignored in unit tests — call methods directly
References
- Spring `@Async` Documentation
- Spring `@Scheduled` Documentation
- Awaitility Testing Library
- CompletableFuture API
- Code examples:
references/examples.md
Async & Scheduled Testing — Code Examples
Maven Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>Gradle Dependencies
dependencies {
implementation("org.springframework.boot:spring-boot-starter")
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.awaitility:awaitility")
testImplementation("org.assertj:assertj-core")
}Basic Async Testing with CompletableFuture
@Service
public class EmailService {
@Async
public CompletableFuture<Boolean> sendEmailAsync(String to, String subject) {
return CompletableFuture.supplyAsync(() -> {
System.out.println("Sending email to " + to);
return true;
});
}
@Async
public void notifyUser(String userId) {
System.out.println("Notifying user: " + userId);
}
}
// Unit test
class EmailServiceAsyncTest {
@Test
void shouldReturnCompletedFutureWhenSendingEmail() throws Exception {
EmailService service = new EmailService();
CompletableFuture<Boolean> result = service.sendEmailAsync("test@example.com", "Hello");
Boolean success = result.get(); // Wait for completion
assertThat(success).isTrue();
}
@Test
void shouldCompleteWithinTimeout() {
EmailService service = new EmailService();
CompletableFuture<Boolean> result = service.sendEmailAsync("test@example.com", "Hello");
assertThat(result).isCompletedWithValue(true);
}
}Async Service with Mocked Dependencies
@Service
public class UserNotificationService {
private final EmailService emailService;
private final SmsService smsService;
public UserNotificationService(EmailService emailService, SmsService smsService) {
this.emailService = emailService;
this.smsService = smsService;
}
@Async
public CompletableFuture<String> notifyUserAsync(String userId) {
return CompletableFuture.supplyAsync(() -> {
emailService.send(userId);
smsService.send(userId);
return "Notification sent";
});
}
}
@ExtendWith(MockitoExtension.class)
class UserNotificationServiceAsyncTest {
@Mock
private EmailService emailService;
@Mock
private SmsService smsService;
@InjectMocks
private UserNotificationService notificationService;
@Test
void shouldNotifyUserAsynchronously() throws Exception {
CompletableFuture<String> result = notificationService.notifyUserAsync("user123");
String message = result.get();
assertThat(message).isEqualTo("Notification sent");
verify(emailService).send("user123");
verify(smsService).send("user123");
}
@Test
void shouldHandleAsyncExceptionGracefully() {
doThrow(new RuntimeException("Email service failed")).when(emailService).send(any());
CompletableFuture<String> result = notificationService.notifyUserAsync("user123");
assertThatThrownBy(result::get)
.isInstanceOf(ExecutionException.class)
.hasCauseInstanceOf(RuntimeException.class);
}
}Testing @Scheduled Methods
@Component
public class DataRefreshTask {
private final DataRepository dataRepository;
public DataRefreshTask(DataRepository dataRepository) {
this.dataRepository = dataRepository;
}
@Scheduled(fixedDelay = 60000)
public void refreshCache() {
dataRepository.findAll(); // Update cache
}
@Scheduled(cron = "0 0 * * * *") // Every hour
public void cleanupOldData() {
dataRepository.deleteOldData(LocalDateTime.now().minusDays(30));
}
}
@ExtendWith(MockitoExtension.class)
class DataRefreshTaskTest {
@Mock
private DataRepository dataRepository;
@InjectMocks
private DataRefreshTask dataRefreshTask;
@Test
void shouldRefreshCacheFromRepository() {
when(dataRepository.findAll()).thenReturn(List.of(new Data(1L, "item1")));
dataRefreshTask.refreshCache(); // Call directly — no cron needed
verify(dataRepository).findAll();
}
@Test
void shouldCleanupOldData() {
dataRefreshTask.cleanupOldData();
verify(dataRepository).deleteOldData(any(LocalDateTime.class));
}
}Testing Async with Awaitility
@Service
public class BackgroundWorker {
private final AtomicInteger processedCount = new AtomicInteger(0);
@Async
public void processItems(List<String> items) {
items.forEach(item -> processedCount.incrementAndGet());
}
public int getProcessedCount() { return processedCount.get(); }
}
class AwaitilityAsyncTest {
@Test
void shouldProcessAllItemsAsynchronously() {
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));
}
@Test
void shouldTimeoutWhenProcessingTakesTooLong() {
BackgroundWorker worker = new BackgroundWorker();
worker.processItems(List.of("item1"));
assertThatThrownBy(() ->
Awaitility.await().atMost(Duration.ofMillis(100)).until(() -> worker.getProcessedCount() == 10)
).isInstanceOf(ConditionTimeoutException.class);
}
}Testing Scheduled Task Execution Count
@Component
public class HealthCheckTask {
private final HealthCheckService healthCheckService;
private int executionCount = 0;
public HealthCheckTask(HealthCheckService healthCheckService) {
this.healthCheckService = healthCheckService;
}
@Scheduled(fixedRate = 5000)
public void checkHealth() {
executionCount++;
healthCheckService.check();
}
public int getExecutionCount() { return executionCount; }
}
class ScheduledTaskTimingTest {
@Test
void shouldExecuteTaskMultipleTimes() {
HealthCheckService mockService = mock(HealthCheckService.class);
HealthCheckTask task = new HealthCheckTask(mockService);
task.checkHealth();
task.checkHealth();
task.checkHealth();
assertThat(task.getExecutionCount()).isEqualTo(3);
verify(mockService, times(3)).check();
}
}Related skills
FAQ
Do I need to wait for actual cron/fixedRate timing in unit tests?
No. Call @Scheduled methods directly in tests; the annotation is ignored. Test the method logic in isolation without waiting for real scheduling intervals.
What timeout should I use for CompletableFuture.get()?
Always set a reasonable timeout (e.g., 5 seconds) to prevent test hangs. If Awaitility times out, increase atMost() duration or reduce pollInterval() until the condition is reachable.
How do I test exception handling in @Async methods?
Verify ExecutionException is thrown on CompletableFuture.get() and check .getCause() to identify the root exception. Mock dependencies to simulate failure paths.
Is Unit Test Scheduled Async safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.