---
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 `@EventListener` method invocation and side effects
- Testing event propagation through multiple listeners
- Validating async event handling (`@Async` + `@EventListener`)
- Mocking `ApplicationEventPublisher` in service tests
## Instructions
1. **Add test dependencies**: `spring-boot-starter`, JUnit 5, Mockito, AssertJ
2. **Mock ApplicationEventPublisher**: use `@Mock` on the publisher field in the service under test
3. **Capture events with ArgumentCaptor**: `ArgumentCaptor.forClass(EventType.class)` to inspect published payload
4. **Verify listener side effects**: invoke listener directly against mocked dependencies
5. **Test async handlers**: use `Thread.sleep()` or Awaitility — then assert the async operation was called
6. **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
7. **Cover error scenarios**: assert listeners handle exceptions gracefully
## Examples
### Maven
```xml
org.springframework.boot
spring-boot-starter
org.junit.jupiter
junit-jupiter
test
org.mockito
mockito-core
test
org.assertj
assertj-core
test
```
### Gradle
```kotlin
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
```java
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
```java
@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 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
```java
@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
```java
@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 `ArgumentCaptor` and 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
- **`@Async` requires `@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
- [Spring ApplicationEvent Javadoc](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/ApplicationEvent.html)
- [ApplicationEventPublisher Javadoc](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/ApplicationEventPublisher.html)
- [`@EventListener` Javadoc](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/event/EventListener.html)