unit-test-application-events
giuseppe-trisciuoglio/developer-kit
Unit test patterns for Spring ApplicationEvent publishers and @EventListener consumers without booting the full context.
What is unit-test-application-events?
Provides actionable patterns for testing Spring application events using JUnit 5 and Mockito. Use when writing tests for event listeners, verifying event publishing, testing @EventListener behavior, or validating async event handling in Spring Boot services.
- Mock ApplicationEventPublisher and capture published events with ArgumentCaptor
- Test @EventListener method invocation and side effects in isolation
- Verify async event handling with @Async + @EventListener using Thread.sleep or Awaitility
- Test event payload correctness and listener error handling without booting Spring context
- Validate event propagation through multiple listeners
- Cover error scenarios where listeners must handle exceptions gracefully
How to install unit-test-application-events
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-application-events- JUnit 5 (junit-jupiter)
- Mockito (mockito-core)
- AssertJ (assertj-core)
- Spring Boot starter dependency
How to use unit-test-application-events
- 1.Add test dependencies (JUnit 5, Mockito, AssertJ) to your Maven or Gradle build
- 2.Mock ApplicationEventPublisher as a @Mock field in your test class
- 3.Use ArgumentCaptor.forClass(EventType.class) to capture published events
- 4.Verify publishEvent() was called and inspect the captured event payload
- 5.For listener tests, construct the listener directly with mocked dependencies and call the @EventListener method
- 6.For async listeners, use Thread.sleep() or Awaitility to wait for async execution, then verify side effects
- 7.Add validation checkpoints: confirm captured events are not null before asserting fields
- 8.Test error scenarios by mocking dependencies to throw exceptions and verify listeners handle them gracefully
Use cases
- Unit testing a service that publishes custom ApplicationEvent with correct payload
- Verifying that an event listener sends a welcome email when UserCreatedEvent is published
- Testing async event handlers that perform slow operations without blocking the publisher
- Mocking ApplicationEventPublisher in service tests to isolate business logic
- Validating listener error handling when downstream dependencies fail
- Spring Boot developers writing unit tests for event-driven services
- QA engineers validating event publishing and listener behavior
- Backend engineers building event-sourced or event-driven architectures
- Developers migrating from integration tests to faster unit tests for events
unit-test-application-events FAQ
Use ArgumentCaptor to capture the published event: ArgumentCaptor<YourEvent> captor = ArgumentCaptor.forClass(YourEvent.class); verify(eventPublisher).publishEvent(captor.capture()); then assert captor.getValue().getField() equals the expected value.
Test listeners in isolation by constructing them directly with mocked dependencies and calling the @EventListener method. This is faster and avoids booting Spring. Only use @SpringBootTest if you need to verify Spring's wiring itself.
Use Awaitility with .atMost() instead of Thread.sleep() for deterministic waits. Alternatively, verify the mocked downstream service was called after a short sleep. Ensure @EnableAsync is configured in your test if using real async execution.
Listeners should handle their own exceptions gracefully and not propagate them to the publisher. Test this by mocking a dependency to throw an exception and verifying the listener method completes without re-throwing.
Spring does not guarantee listener order by default. Add @Order(n) to listeners if order matters, then test that listeners are invoked in the expected sequence using multiple ArgumentCaptor calls or verify() invocations.
Full instructions (SKILL.md)
Source of truth, from giuseppe-trisciuoglio/developer-kit.
name: unit-test-application-events description: Provides patterns for unit testing Spring application events. Validates event publishing with ApplicationEventPublisher, tests @EventListener annotation behavior, and verifies async event handling. Use when writing tests for event listeners, mocking application events, or verifying events were published in your Spring Boot services. allowed-tools: Read, Write, Bash, Glob, Grep
Unit Testing Application Events
Overview
Provides actionable patterns for testing Spring ApplicationEvent publishers and @EventListener consumers using JUnit 5 and Mockito — without booting the full Spring context.
When to Use
- Writing unit tests for event publishers or listeners
- Verifying that an event was published with correct payload
- Testing
@EventListenermethod invocation and side effects - Testing event propagation through multiple listeners
- Validating async event handling (
@Async+@EventListener) - Mocking
ApplicationEventPublisherin service tests
Instructions
- Add test dependencies:
spring-boot-starter, JUnit 5, Mockito, AssertJ - Mock ApplicationEventPublisher: use
@Mockon the publisher field in the service under test - Capture events with ArgumentCaptor:
ArgumentCaptor.forClass(EventType.class)to inspect published payload - Verify listener side effects: invoke listener directly against mocked dependencies
- Test async handlers: use
Thread.sleep()or Awaitility — then assert the async operation was called - Add validation checkpoints:
- After capturing an event, confirm
eventCaptor.getValue()is not null before asserting fields - If the listener is not invoked, verify
publishEvent()was called with the correct event type - If async assertions fail, increase wait time and check the executor pool is not saturated
- After capturing an event, confirm
- Cover error scenarios: assert listeners handle exceptions gracefully
Examples
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")
}
Custom Event and Publisher Test
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
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 savedUser = userRepository.save(new User(name, email));
eventPublisher.publishEvent(new UserCreatedEvent(this, savedUser));
return savedUser;
}
}
Unit Test for Event Publishing
@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", "alice@example.com");
when(userRepository.save(any(User.class))).thenReturn(newUser);
ArgumentCaptor<UserCreatedEvent> eventCaptor = ArgumentCaptor.forClass(UserCreatedEvent.class);
userService.createUser("Alice", "alice@example.com");
verify(eventPublisher).publishEvent(eventCaptor.capture());
assertThat(eventCaptor.getValue().getUser()).isEqualTo(newUser);
}
}
Listener Direct Test
@Component
public class UserEventListener {
private final EmailService emailService;
public UserEventListener(EmailService emailService) { this.emailService = emailService; }
@EventListener
public void onUserCreated(UserCreatedEvent event) {
emailService.sendWelcomeEmail(event.getUser().getEmail());
}
}
class UserEventListenerTest {
@Test
void shouldSendWelcomeEmailOnUserCreated() {
EmailService emailService = mock(EmailService.class);
UserEventListener listener = new UserEventListener(emailService);
User user = new User(1L, "Alice", "alice@example.com");
listener.onUserCreated(new UserCreatedEvent(this, user));
verify(emailService).sendWelcomeEmail("alice@example.com");
}
@Test
void shouldNotThrowWhenEmailServiceFails() {
EmailService emailService = mock(EmailService.class);
doThrow(new RuntimeException("down")).when(emailService).sendWelcomeEmail(any());
UserEventListener listener = new UserEventListener(emailService);
User user = new User(1L, "Alice", "alice@example.com");
assertThatCode(() -> listener.onUserCreated(new UserCreatedEvent(this, user)))
.doesNotThrowAnyException();
}
}
Async Listener Test
@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", "alice@example.com");
listener.onUserCreatedAsync(new UserCreatedEvent(this, user));
Thread.sleep(200); // checkpoint: allow async executor to run
verify(slowService).processUser(user);
}
}
Best Practices
- Mock
ApplicationEventPublisher— never let it post to a real context in unit tests - Capture events with
ArgumentCaptorand assert field-level equality, not just type - Test listeners in isolation: construct them with mocked dependencies and call the handler method directly
- Cover error paths: listeners must not propagate exceptions to publishers
- Async listeners: prefer Awaitility over
Thread.sleep()for deterministic waits - Keep events immutable and serializable — test both if events cross JVM boundaries
Constraints and Warnings
- Do not test Spring's own event infrastructure — focus on your business logic and event payload
@Asyncrequires@EnableAsync— tests using Thread.sleep may still pass even if the async proxy is not wired in the test; use a mock verify instead- Spring does not guarantee listener order — do not write tests that depend on execution sequence unless you add
@Order - Avoid
Thread.sleep()in CI environments — it makes tests flaky under load; replace with Awaitility.atMost()blocks - Events crossing JVM boundaries need serialization tests — null fields in remote listeners often mean missing
Serializable
References
Related skills
More from giuseppe-trisciuoglio/developer-kit and the wider catalog.

unit-test-bean-validation
Unit test Jakarta Bean Validation constraints and custom validators with JUnit 5 in isolation.

unit-test-boundary-conditions
Test boundary conditions, edge cases, and limits in Java with JUnit 5 and AssertJ patterns.

unit-test-caching
Unit test patterns for Spring Cache annotations (@Cacheable, @CachePut, @CacheEvict) with mocked cache managers.

unit-test-config-properties
Unit test Spring Boot @ConfigurationProperties with property binding, validation, and type conversion patterns.

unit-test-controller-layer
Unit test REST controllers in isolation using MockMvc and @WebMvcTest patterns.

unit-test-exception-handler
Unit test patterns for Spring Boot @ExceptionHandler and @ControllerAdvice with MockMvc