Developer-kit unit-test-application-events
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.
install
source · Clone the upstream repo
git clone https://github.com/giuseppe-trisciuoglio/developer-kit
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/giuseppe-trisciuoglio/developer-kit "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/developer-kit-java/skills/unit-test-application-events" ~/.claude/skills/giuseppe-trisciuoglio-developer-kit-unit-test-application-events && rm -rf "$T"
manifest:
plugins/developer-kit-java/skills/unit-test-application-events/SKILL.mdsource content
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
method invocation and side effects@EventListener - Testing event propagation through multiple listeners
- Validating async event handling (
+@Async
)@EventListener - Mocking
in service testsApplicationEventPublisher
Instructions
- Add test dependencies:
, JUnit 5, Mockito, AssertJspring-boot-starter - Mock ApplicationEventPublisher: use
on the publisher field in the service under test@Mock - Capture events with ArgumentCaptor:
to inspect published payloadArgumentCaptor.forClass(EventType.class) - Verify listener side effects: invoke listener directly against mocked dependencies
- Test async handlers: use
or Awaitility — then assert the async operation was calledThread.sleep() - Add validation checkpoints:
- After capturing an event, confirm
is not null before asserting fieldseventCaptor.getValue() - If the listener is not invoked, verify
was called with the correct event typepublishEvent() - 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
— never let it post to a real context in unit testsApplicationEventPublisher - Capture events with
and assert field-level equality, not just typeArgumentCaptor - 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
for deterministic waitsThread.sleep() - 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
requires@Async
— tests using Thread.sleep may still pass even if the async proxy is not wired in the test; use a mock verify instead@EnableAsync- Spring does not guarantee listener order — do not write tests that depend on execution sequence unless you add
@Order - Avoid
in CI environments — it makes tests flaky under load; replace with AwaitilityThread.sleep()
blocks.atMost() - Events crossing JVM boundaries need serialization tests — null fields in remote listeners often mean missing
Serializable