Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/giuseppe-trisciuoglio-unit-test-application-events/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-15 | ✗→✓ | ▲ Improved | 60% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 66% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 78% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 58% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 44% | 0% |
Provides actionable patterns for testing Spring ApplicationEvent publishers and @EventListener consumers using JUnit 5 and Mockito — without booting the full Spring context.
@EventListener method invocation and side effects@Async + @EventListener)ApplicationEventPublisher in service testsspring-boot-starter, JUnit 5, Mockito, AssertJ@Mock on the publisher field in the service under testArgumentCaptor.forClass(EventType.class) to inspect published payloadThread.sleep() or Awaitility — then assert the async operation was calledeventCaptor.getValue() is not null before asserting fieldspublishEvent() was called with the correct event typexml<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>
kotlindependencies { implementation("org.springframework.boot:spring-boot-starter") testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.mockito:mockito-core") testImplementation("org.assertj:assertj-core") }
javapublic 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; } }
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<UserCreatedEvent> eventCaptor = ArgumentCaptor.forClass(UserCreatedEvent.class); userService.createUser("Alice", "alice@example.com"); verify(eventPublisher).publishEvent(eventCaptor.capture()); assertThat(eventCaptor.getValue().getUser()).isEqualTo(newUser); } }
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(); } }
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); } }
ApplicationEventPublisher — never let it post to a real context in unit testsArgumentCaptor and assert field-level equality, not just typeThread.sleep() for deterministic waits@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@OrderThread.sleep() in CI environments — it makes tests flaky under load; replace with Awaitility .atMost() blocksSerializable| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 13,850 | 13,825 | -0% | 1 | 1 | 0% | 2,680 | 4,457 | +66% | 0 | 0 | — |
case-02 | pass→pass | 11,205 | 10,003 | -11% | 1 | 1 | 0% | 2,130 | 3,790 | +78% | 0 | 0 | — |
case-03 | pass→pass | 13,177 | 10,504 | -20% | 1 | 1 | 0% | 2,295 | 3,616 | +58% | 0 | 0 | — |
case-04 | pass→pass | 14,715 | 13,298 | -10% | 1 | 1 | 0% | 2,214 | 3,197 | +44% | 0 | 0 | — |
case-05 | pass→pass | 12,869 | 9,546 | -26% | 1 | 1 | 0% | 2,095 | 3,486 | +66% | 0 | 0 | — |
case-06 | pass→pass | 10,988 | 10,929 | -1% | 1 | 1 | 0% | 2,014 | 3,958 | +97% | 0 | 0 | — |
case-07 | pass→pass | 18,565 | 10,084 | -46% | 1 | 1 | 0% | 1,895 | 3,658 | +93% | 0 | 0 | — |
case-08 | pass→pass | 9,299 | 7,974 | -14% | 1 | 1 | 0% | 1,543 | 3,151 | +104% | 0 | 0 | — |
case-09 | pass→pass | 5,692 | 6,283 | +10% | 1 | 1 | 0% | 1,114 | 2,833 | +154% | 0 | 0 | — |
case-10 | pass→pass | 10,970 | 6,825 | -38% | 1 | 1 | 0% | 1,808 | 2,857 | +58% | 0 | 0 | — |
case-11 | pass→pass | 7,698 | 2,951 | -62% | 1 | 1 | 0% | 1,347 | 2,271 | +69% | 0 | 0 | — |
case-12 | pass→pass | 13,726 | 11,228 | -18% | 1 | 1 | 0% | 2,383 | 3,655 | +53% | 0 | 0 | — |
case-13 | pass→pass | 8,282 | 7,342 | -11% | 1 | 1 | 0% | 1,602 | 2,975 | +86% | 0 | 0 | — |
case-14 | pass→pass | 9,318 | 4,025 | -57% | 1 | 1 | 0% | 1,467 | 2,451 | +67% | 0 | 0 | — |
case-15 | fail→pass | 10,071 | 7,573 | -25% | 1 | 1 | 0% | 2,043 | 3,262 | +60% | 0 | 0 | — |
case-16 | pass→pass | 10,716 | 8,660 | -19% | 1 | 1 | 0% | 2,098 | 3,462 | +65% | 0 | 0 | — |
case-17 | pass→pass | 9,202 | 7,826 | -15% | 1 | 1 | 0% | 1,508 | 3,231 | +114% | 0 | 0 | — |
case-18 | pass→pass | 14,957 | 8,696 | -42% | 1 | 1 | 0% | 2,067 | 3,115 | +51% | 0 | 0 | — |
case-19 | pass→pass | 2,902 | 2,810 | -3% | 1 | 1 | 0% | 438 | 2,252 | +414% | 0 | 0 | — |
case-20 | pass→pass | 19,943 | 8,993 | -55% | 1 | 1 | 0% | 2,176 | 3,518 | +62% | 0 | 0 | — |
case-21 | pass→pass | 8,979 | 9,436 | +5% | 1 | 1 | 0% | 1,729 | 3,741 | +116% | 0 | 0 | — |
case-22 | pass→pass | 15,030 | 13,716 | -9% | 1 | 1 | 0% | 2,950 | 4,630 | +57% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.