PluginBench
Skill
Review
Audit score 70

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
Prerequisites
  • JUnit 5 (junit-jupiter)
  • Mockito (mockito-core)
  • AssertJ (assertj-core)
  • Spring Boot starter dependency
Claude Code
Cursor
Windsurf
Cline

How to use unit-test-application-events

  1. 1.Add test dependencies (JUnit 5, Mockito, AssertJ) to your Maven or Gradle build
  2. 2.Mock ApplicationEventPublisher as a @Mock field in your test class
  3. 3.Use ArgumentCaptor.forClass(EventType.class) to capture published events
  4. 4.Verify publishEvent() was called and inspect the captured event payload
  5. 5.For listener tests, construct the listener directly with mocked dependencies and call the @EventListener method
  6. 6.For async listeners, use Thread.sleep() or Awaitility to wait for async execution, then verify side effects
  7. 7.Add validation checkpoints: confirm captured events are not null before asserting fields
  8. 8.Test error scenarios by mocking dependencies to throw exceptions and verify listeners handle them gracefully

Use cases

Good for
  • 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
Who it's for
  • 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

How do I test that an event was published with the correct payload?

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.

Should I test listeners with the full Spring context or in isolation?

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.

How do I test @Async event listeners reliably?

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.

What if my listener throws an exception—should the publisher catch it?

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.

Can I test event listener execution order?

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 @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

<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