Unit tests for scheduled and async tasks using @Scheduled and @Async. Mock task execution and timing. Use when validating asynchronous operations and scheduling behavior.
/plugin marketplace add giuseppe-trisciuoglio/developer-kit/plugin install developer-kit@giuseppe.trisciuoglioThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Test scheduled tasks and async methods using JUnit 5 without running the actual scheduler. Verify execution logic, timing, and asynchronous behavior.
Use this skill when:
<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>
dependencies {
implementation("org.springframework.boot:spring-boot-starter")
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.awaitility:awaitility")
testImplementation("org.assertj:assertj-core")
}
// 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<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);
}
}
@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<String> result = notificationService.notifyUserAsync("user123");
assertThatThrownBy(result::get)
.isInstanceOf(ExecutionException.class)
.hasCauseInstanceOf(RuntimeException.class);
}
}
// 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));
}
}
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);
}
}
@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<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<String> result = service.safeFetchData("invalid");
String message = result.get();
assertThat(message).startsWith("Error:");
}
}
@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();
}
}
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.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.