unit-test-application-events
Install this skill
npx skills add giuseppe-trisciuoglio/developer-kitWorks across Claude Code, Cursor, Codex, Copilot & Antigravity
This skill facilitates isolated testing of Spring Framework event-driven architectures. By mocking ApplicationEventPublisher and directly invoking @EventListener methods, developers verify interaction logic without spinning up the entire Spring ApplicationContext. It provides a lightweight testing strategy for verifying that services correctly emit events and that handlers process those signals as expected. This approach centers on isolating the business logic within publishers and observers, ensuring that individual components behave correctly when event payloads are transmitted. The technique relies on standard JUnit 5 testing utilities and Mockito to capture event objects or simulate listener failures, allowing for granular validation of event propagation paths within a reactive or message-driven workflow. It is specifically beneficial for improving build speeds by avoiding expensive context initialization during standard unit test cycles.
When to Use This Skill
- β’Validating that a service emits the expected event after a database save
- β’Ensuring an email notification listener correctly parses user data from an event
- β’Testing listener resilience against service dependency exceptions
- β’Verifying that multiple listeners receive the same published event
How to Invoke This Skill
Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:
- βhow do I test Spring event listeners with JUnit
- βverify ApplicationEventPublisher calls in unit tests
- βmocking event publishing in spring boot service tests
- βtest @EventListener logic without starting the application context
- βunit test for spring custom application events
Pro Tips
- π‘For asynchronous event testing, use `Awaitility` or similar libraries to assert eventual state changes, ensuring your tests don't prematurely finish before the async listener completes.
- π‘Mock external dependencies within your event listeners to isolate the event handling logic and ensure your unit tests focus solely on the event processing.
- π‘Consider creating custom test annotations or utilities to simplify event publication and subscription assertions, reducing boilerplate in your test suite.
What this skill does
- β’Verify ApplicationEventPublisher interaction using Mockito ArgumentCaptor
- β’Directly invoke @EventListener methods to validate handler logic
- β’Simulate event processing failures within individual listeners
- β’Assert event payload integrity during unit execution
- β’Test synchronous event handling chains in isolation
When not to use it
- βTesting integration between full Spring components or cross-service event bus infrastructure
- βValidating asynchronous thread-pool behavior or event queue persistence
- βTesting complex bean initialization logic or application lifecycle events
Example workflow
- Create a custom event class extending ApplicationEvent
- Inject a mocked ApplicationEventPublisher into the target service
- Define a JUnit test that triggers the service method
- Use ArgumentCaptor to intercept the published event object
- Assert that the captured event contains the correct state
- Manually invoke the listener method with the event to verify downstream effects
Prerequisites
- βSpring Boot starter
- βJUnit 5
- βMockito
- βAssertJ
Pitfalls & limitations
- !Unit tests won't catch configuration errors in @EventListener bean registration
- !Requires manual listener invocation, which may bypass actual Spring event dispatching mechanics
- !Testing only the handler logic misses the integration overhead of the internal ApplicationEventMulticaster
FAQ
How it compares
Unlike integration tests that require a full context, this skill provides focused, high-speed verification of individual event-handling logic, preventing the overhead of unnecessary bean wiring.
π Full skill instructions β original source: giuseppe-trisciuoglio/developer-kit
Test Spring ApplicationEvent publishers and event listeners using JUnit 5. Verify event publishing, listener execution, and event propagation without full context startup.
## When to Use This Skill
Use this skill when:
- Testing ApplicationEventPublisher event publishing
- Testing @EventListener method invocation
- Verifying event listener logic and side effects
- Testing event propagation through listeners
- Want fast event-driven architecture tests
- Testing both synchronous and asynchronous event handling
## Setup: Event 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.mockito</groupId>
<artifactId>mockito-core</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.mockito:mockito-core")
testImplementation("org.assertj:assertj-core")
}## Basic Pattern: Event Publishing and Listening
### Custom Event and Publisher
// Custom application event
public class UserCreatedEvent extends ApplicationEvent {
private final User user;
public UserCreatedEvent(Object source, User user) {
super(source);
this.user = user;
}
public User getUser() {
return user;
}
}
// Service that publishes events
@Service
public class UserService {
private final ApplicationEventPublisher eventPublisher;
private final UserRepository userRepository;
public UserService(ApplicationEventPublisher eventPublisher, UserRepository userRepository) {
this.eventPublisher = eventPublisher;
this.userRepository = userRepository;
}
public User createUser(String name, String email) {
User user = new User(name, email);
User savedUser = userRepository.save(user);
eventPublisher.publishEvent(new UserCreatedEvent(this, savedUser));
return savedUser;
}
}
// Unit test
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceEventTest {
@Mock
private ApplicationEventPublisher eventPublisher;
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void shouldPublishUserCreatedEvent() {
User newUser = new User(1L, "Alice", "[email protected]");
when(userRepository.save(any(User.class))).thenReturn(newUser);
ArgumentCaptor<UserCreatedEvent> eventCaptor = ArgumentCaptor.forClass(UserCreatedEvent.class);
userService.createUser("Alice", "[email protected]");
verify(eventPublisher).publishEvent(eventCaptor.capture());
UserCreatedEvent capturedEvent = eventCaptor.getValue();
assertThat(capturedEvent.getUser()).isEqualTo(newUser);
}
}## Testing Event Listeners
### @EventListener Annotation
// Event listener
@Component
public class UserEventListener {
private final EmailService emailService;
public UserEventListener(EmailService emailService) {
this.emailService = emailService;
}
@EventListener
public void onUserCreated(UserCreatedEvent event) {
User user = event.getUser();
emailService.sendWelcomeEmail(user.getEmail());
}
}
// Unit test for listener
class UserEventListenerTest {
@Test
void shouldSendWelcomeEmailWhenUserCreated() {
EmailService emailService = mock(EmailService.class);
UserEventListener listener = new UserEventListener(emailService);
User newUser = new User(1L, "Alice", "[email protected]");
UserCreatedEvent event = new UserCreatedEvent(this, newUser);
listener.onUserCreated(event);
verify(emailService).sendWelcomeEmail("[email protected]");
}
@Test
void shouldNotThrowExceptionWhenEmailServiceFails() {
EmailService emailService = mock(EmailService.class);
doThrow(new RuntimeException("Email service down"))
.when(emailService).sendWelcomeEmail(any());
UserEventListener listener = new UserEventListener(emailService);
User newUser = new User(1L, "Alice", "[email protected]");
UserCreatedEvent event = new UserCreatedEvent(this, newUser);
// Should handle exception gracefully
assertThatCode(() -> listener.onUserCreated(event))
.doesNotThrowAnyException();
}
}## Testing Multiple Listeners
### Event Propagation
class UserCreatedEvent extends ApplicationEvent {
private final User user;
private final List<String> notifications = new ArrayList<>();
public UserCreatedEvent(Object source, User user) {
super(source);
this.user = user;
}
public void addNotification(String notification) {
notifications.add(notification);
}
public List<String> getNotifications() {
return notifications;
}
}
class MultiListenerTest {
@Test
void shouldNotifyMultipleListenersSequentially() {
EmailService emailService = mock(EmailService.class);
NotificationService notificationService = mock(NotificationService.class);
AnalyticsService analyticsService = mock(AnalyticsService.class);
UserEventListener emailListener = new UserEventListener(emailService);
UserEventListener notificationListener = new UserEventListener(notificationService);
UserEventListener analyticsListener = new UserEventListener(analyticsService);
User user = new User(1L, "Alice", "[email protected]");
UserCreatedEvent event = new UserCreatedEvent(this, user);
emailListener.onUserCreated(event);
notificationListener.onUserCreated(event);
analyticsListener.onUserCreated(event);
verify(emailService).send(any());
verify(notificationService).notify(any());
verify(analyticsService).track(any());
}
}## Testing Conditional Event Listeners
### @EventListener with Condition
@Component
public class ConditionalEventListener {
@EventListener(condition = "#event.user.age > 18")
public void onAdultUserCreated(UserCreatedEvent event) {
// Handle adult user
}
}
class ConditionalListenerTest {
@Test
void shouldProcessEventWhenConditionMatches() {
// Test logic for matching condition
}
@Test
void shouldSkipEventWhenConditionDoesNotMatch() {
// Test logic for non-matching condition
}
}## Testing Async Event Listeners
### @Async with @EventListener
@Component
public class AsyncEventListener {
private final SlowService slowService;
@EventListener
@Async
public void onUserCreatedAsync(UserCreatedEvent event) {
slowService.processUser(event.getUser());
}
}
class AsyncEventListenerTest {
@Test
void shouldProcessEventAsynchronously() throws Exception {
SlowService slowService = mock(SlowService.class);
AsyncEventListener listener = new AsyncEventListener(slowService);
User user = new User(1L, "Alice", "[email protected]");
UserCreatedEvent event = new UserCreatedEvent(this, user);
listener.onUserCreatedAsync(event);
// Event processed asynchronously
Thread.sleep(100); // Wait for async completion
verify(slowService).processUser(user);
}
}## Best Practices
- **Mock ApplicationEventPublisher** in unit tests
- **Capture published events** using ArgumentCaptor
- **Test listener side effects** explicitly
- **Test error handling** in listeners
- **Keep event listeners focused** on single responsibility
- **Verify event data integrity** when capturing
- **Test both sync and async** event processing
## Common Pitfalls
- Testing actual event publishing without mocking publisher
- Not verifying listener invocation
- Not capturing event details
- Testing listener registration instead of logic
- Not handling listener exceptions
## Troubleshooting
**Event not being captured**: Verify ArgumentCaptor type matches event class.
**Listener not invoked**: Ensure event is actually published and listener is registered.
**Async listener timing issues**: Use Thread.sleep() or Awaitility to wait for completion.
## References
- [Spring ApplicationEvent](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/ApplicationEvent.html)
- [Spring ApplicationEventPublisher](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/ApplicationEventPublisher.html)
- [@EventListener Documentation](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/event/EventListener.html)
How to Use This Skill Unit
Option A: Project-Specific (Recommended)
- Click "Download" above
- In your project, create the directory:
.agent/skills/unit-test-application-events/ - Save the file as
SKILL.md - The agent will automatically discover the skill based on its description.
Option B: Global Installation (All Agents)
Save the file to these locations to make it available across all projects:
- Claude Code:
~/.claude/skills/giuseppe-trisciuoglio/developer-kit/unit-test-application-events/SKILL.md - Cursor:
~/.cursor/skills/giuseppe-trisciuoglio/developer-kit/unit-test-application-events/SKILL.md - Antigravity:
~/.gemini/antigravity/skills/giuseppe-trisciuoglio/developer-kit/unit-test-application-events/SKILL.md
π Install with CLI:npx skills add giuseppe-trisciuoglio/developer-kit