unit-test-application-events — an installable skill for AI agents, published by giuseppe-trisciuoglio/developer-kit.
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
Add test dependencies: spring-boot-starter, JUnit 5, Mockito, AssertJ
Mock ApplicationEventPublisher: use @Mock on 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
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 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
ApplicationEventPublisher Javadoc
@EventListener Javadocdon't have the plugin yet? install it then click "run inline in claude" again.