unit-test-scheduled-async

Unit Testing @Scheduled and @Async Methods

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "unit-test-scheduled-async" with this command: npx skills add giuseppe-trisciuoglio/developer-kit/giuseppe-trisciuoglio-developer-kit-unit-test-scheduled-async

Unit Testing @Scheduled and @Async Methods

Overview

This skill provides patterns for unit testing @Scheduled and @Async methods using JUnit 5. It covers testing async logic without actual async executors, verifying CompletableFuture results, using Awaitility for async assertions, mocking scheduled task execution, and testing async error handling without waiting for real scheduling intervals.

When to Use

Use this skill when:

  • Testing @Scheduled method logic

  • Testing @Async method behavior

  • Verifying CompletableFuture results

  • Testing async error handling

  • Want fast tests without actual scheduling

  • Testing background task logic in isolation

Instructions

  • Test async methods directly: Call @Async methods directly instead of relying on Spring's async executor

  • Use CompletableFuture.get(): Wait for async operations to complete with explicit timeout

  • Mock async dependencies: Use @Mock for services that async methods depend on

  • Use Awaitility for assertions: Apply Awaitility.await() when testing actual async behavior

  • Test scheduled methods directly: Call @Scheduled methods directly instead of waiting for cron expressions

  • Verify mock interactions: Ensure dependencies were called correctly after async completion

  • Test exception handling: Verify exceptions in async methods propagate correctly

  • Set appropriate timeouts: Always use timeout with CompletableFuture.get() to avoid hanging tests

Examples

Setup: Async/Scheduled Testing

Maven

<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 { implementation("org.springframework.boot:spring-boot-starter") testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.awaitility:awaitility") testImplementation("org.assertj:assertj-core") }

Testing @Async Methods

Basic Async Testing with CompletableFuture

// Service with async methods @Service public class EmailService {

@Async public CompletableFuture<Boolean> sendEmailAsync(String to, String subject) { return CompletableFuture.supplyAsync(() -> { // Simulate email sending System.out.println("Sending email to " + to); return true; }); }

@Async public void notifyUser(String userId) { System.out.println("Notifying user: " + userId); } }

// Unit test import java.util.concurrent.CompletableFuture; import static org.assertj.core.api.Assertions.*;

class EmailServiceAsyncTest {

@Test void shouldReturnCompletedFutureWhenSendingEmail() throws Exception { EmailService service = new EmailService();

CompletableFuture&#x3C;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&#x3C;Boolean> result = service.sendEmailAsync("test@example.com", "Hello");

assertThat(result)
  .isCompletedWithValue(true);

} }

Testing Async with Mocked Dependencies

Async Service with 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"; }); } }

// Unit test import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension;

@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&#x3C;String> result = notificationService.notifyUserAsync("user123");

assertThatThrownBy(result::get)
  .isInstanceOf(ExecutionException.class)
  .hasCauseInstanceOf(RuntimeException.class);

} }

Testing @Scheduled Methods

Mock Task Execution

// Scheduled task @Component public class DataRefreshTask {

private final DataRepository dataRepository;

public DataRefreshTask(DataRepository dataRepository) { this.dataRepository = dataRepository; }

@Scheduled(fixedDelay = 60000) public void refreshCache() { List<Data> data = dataRepository.findAll(); // Update cache }

@Scheduled(cron = "0 0 * * * *") // Every hour public void cleanupOldData() { dataRepository.deleteOldData(LocalDateTime.now().minusDays(30)); } }

// Unit test - test logic without actual scheduling import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class) class DataRefreshTaskTest {

@Mock private DataRepository dataRepository;

@InjectMocks private DataRefreshTask dataRefreshTask;

@Test void shouldRefreshCacheFromRepository() { List<Data> expectedData = List.of(new Data(1L, "item1")); when(dataRepository.findAll()).thenReturn(expectedData);

dataRefreshTask.refreshCache(); // Call method directly

verify(dataRepository).findAll();

}

@Test void shouldCleanupOldData() { LocalDateTime cutoffDate = LocalDateTime.now().minusDays(30);

dataRefreshTask.cleanupOldData();

verify(dataRepository).deleteOldData(any(LocalDateTime.class));

} }

Testing Async with Awaility

Wait for Async Completion

import org.awaitility.Awaitility; import java.util.concurrent.atomic.AtomicInteger;

@Service public class BackgroundWorker {

private final AtomicInteger processedCount = new AtomicInteger(0);

@Async public void processItems(List<String> items) { items.forEach(item -> { // Process item processedCount.incrementAndGet(); }); }

public int getProcessedCount() { return processedCount.get(); } }

class AwaitilityAsyncTest {

@Test void shouldProcessAllItemsAsynchronously() { BackgroundWorker worker = new BackgroundWorker(); List<String> items = List.of("item1", "item2", "item3");

worker.processItems(items);

// Wait for async operation to complete (up to 5 seconds)
Awaitility.await()
  .atMost(Duration.ofSeconds(5))
  .pollInterval(Duration.ofMillis(100))
  .untilAsserted(() -> {
    assertThat(worker.getProcessedCount()).isEqualTo(3);
  });

}

@Test void shouldTimeoutWhenProcessingTakesTooLong() { BackgroundWorker worker = new BackgroundWorker(); List<String> items = List.of("item1", "item2", "item3");

worker.processItems(items);

assertThatThrownBy(() -> 
  Awaitility.await()
    .atMost(Duration.ofMillis(100))
    .until(() -> worker.getProcessedCount() == 10)
).isInstanceOf(ConditionTimeoutException.class);

} }

Testing Async Error Handling

Handle Exceptions in Async Methods

@Service public class DataProcessingService {

@Async public CompletableFuture<Boolean> processDataAsync(String data) { return CompletableFuture.supplyAsync(() -> { if (data == null || data.isEmpty()) { throw new IllegalArgumentException("Data cannot be empty"); } // Process data return true; }); }

@Async public CompletableFuture<String> safeFetchData(String id) { return CompletableFuture.supplyAsync(() -> { try { return fetchData(id); } catch (Exception e) { return "Error: " + e.getMessage(); } }); } }

class AsyncErrorHandlingTest {

@Test void shouldPropagateExceptionFromAsyncMethod() { DataProcessingService service = new DataProcessingService();

CompletableFuture&#x3C;Boolean> result = service.processDataAsync(null);

assertThatThrownBy(result::get)
  .isInstanceOf(ExecutionException.class)
  .hasCauseInstanceOf(IllegalArgumentException.class)
  .hasMessageContaining("Data cannot be empty");

}

@Test void shouldHandleExceptionGracefullyWithFallback() throws Exception { DataProcessingService service = new DataProcessingService();

CompletableFuture&#x3C;String> result = service.safeFetchData("invalid");

String message = result.get();
assertThat(message).startsWith("Error:");

} }

Testing Scheduled Task Timing

Test Schedule Configuration

@Component public class HealthCheckTask {

private final HealthCheckService healthCheckService; private int executionCount = 0;

public HealthCheckTask(HealthCheckService healthCheckService) { this.healthCheckService = healthCheckService; }

@Scheduled(fixedRate = 5000) // Every 5 seconds 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);

// Execute manually multiple times
task.checkHealth();
task.checkHealth();
task.checkHealth();

assertThat(task.getExecutionCount()).isEqualTo(3);
verify(mockService, times(3)).check();

} }

Best Practices

  • Test async method logic directly without Spring async executor

  • Use CompletableFuture.get() to wait for results in tests

  • Mock dependencies that async methods use

  • Test error paths for async operations

  • Use Awaitility when testing actual async behavior is needed

  • Mock scheduled tasks by calling methods directly in tests

  • Verify task execution count for testing scheduling logic

Common Pitfalls

  • Testing with actual @Async executor (use direct method calls instead)

  • Not waiting for CompletableFuture completion in tests

  • Forgetting to test exception handling in async methods

  • Not mocking dependencies that async methods call

  • Trying to test actual scheduling timing (test logic instead)

Constraints and Warnings

  • @Async requires proxy: Spring's @Async works via proxies; direct method calls bypass async behavior

  • ThreadPoolTaskScheduler: Scheduled tasks use a thread pool; order of execution is not guaranteed

  • CompletableFuture chaining: Chain operations carefully; exceptions in intermediate stages can be lost

  • Awaitility timeout: Set reasonable timeouts; infinite waits can hang test suites

  • Scheduled task timing: Don't test actual cron/fixedRate timing; test the method logic directly

  • Thread safety: Async code must be thread-safe; verify behavior under concurrent access

  • @Async on same class: Calling @Async method from another method in same class won't be async

Troubleshooting

CompletableFuture hangs in test: Ensure methods complete or set timeout with .get(timeout, unit) .

Async method not executing: Call method directly instead of relying on @Async in tests.

Awaitility timeout: Increase timeout duration or reduce polling interval.

References

  • Spring @Async Documentation

  • Spring @Scheduled Documentation

  • Awaitility Testing Library

  • CompletableFuture API

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

shadcn-ui

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

tailwind-css-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

unit-test-bean-validation

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

react-patterns

No summary provided by upstream source.

Repository SourceNeeds Review