Back to Testing & Quality Assurance

unit-test-application-events

Spring BootJUnit 5ApplicationEventEvent TestingUnit TestingMockitoAsynchronous EventsTDD
⭐ 282πŸ“„ MITπŸ•’ 2026-06-15Source β†—

Install this skill

npx skills add giuseppe-trisciuoglio/developer-kit

Works across Claude Code, Cursor, Codex, Copilot & Antigravity

This skill facilitates isolated testing of Spring Framework event-driven architectures. By mocking ApplicationEventPublisher and directly invoking @EventListener methods, developers verify interaction logic without spinning up the entire Spring ApplicationContext. It provides a lightweight testing strategy for verifying that services correctly emit events and that handlers process those signals as expected. This approach centers on isolating the business logic within publishers and observers, ensuring that individual components behave correctly when event payloads are transmitted. The technique relies on standard JUnit 5 testing utilities and Mockito to capture event objects or simulate listener failures, allowing for granular validation of event propagation paths within a reactive or message-driven workflow. It is specifically beneficial for improving build speeds by avoiding expensive context initialization during standard unit test cycles.

When to Use This Skill

  • β€’Validating that a service emits the expected event after a database save
  • β€’Ensuring an email notification listener correctly parses user data from an event
  • β€’Testing listener resilience against service dependency exceptions
  • β€’Verifying that multiple listeners receive the same published event

How to Invoke This Skill

Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:

  • β€œhow do I test Spring event listeners with JUnit
  • β€œverify ApplicationEventPublisher calls in unit tests
  • β€œmocking event publishing in spring boot service tests
  • β€œtest @EventListener logic without starting the application context
  • β€œunit test for spring custom application events

Pro Tips

  • πŸ’‘For asynchronous event testing, use `Awaitility` or similar libraries to assert eventual state changes, ensuring your tests don't prematurely finish before the async listener completes.
  • πŸ’‘Mock external dependencies within your event listeners to isolate the event handling logic and ensure your unit tests focus solely on the event processing.
  • πŸ’‘Consider creating custom test annotations or utilities to simplify event publication and subscription assertions, reducing boilerplate in your test suite.

What this skill does

  • β€’Verify ApplicationEventPublisher interaction using Mockito ArgumentCaptor
  • β€’Directly invoke @EventListener methods to validate handler logic
  • β€’Simulate event processing failures within individual listeners
  • β€’Assert event payload integrity during unit execution
  • β€’Test synchronous event handling chains in isolation

When not to use it

  • βœ•Testing integration between full Spring components or cross-service event bus infrastructure
  • βœ•Validating asynchronous thread-pool behavior or event queue persistence
  • βœ•Testing complex bean initialization logic or application lifecycle events

Example workflow

  1. Create a custom event class extending ApplicationEvent
  2. Inject a mocked ApplicationEventPublisher into the target service
  3. Define a JUnit test that triggers the service method
  4. Use ArgumentCaptor to intercept the published event object
  5. Assert that the captured event contains the correct state
  6. Manually invoke the listener method with the event to verify downstream effects

Prerequisites

  • –Spring Boot starter
  • –JUnit 5
  • –Mockito
  • –AssertJ

Pitfalls & limitations

  • !Unit tests won't catch configuration errors in @EventListener bean registration
  • !Requires manual listener invocation, which may bypass actual Spring event dispatching mechanics
  • !Testing only the handler logic misses the integration overhead of the internal ApplicationEventMulticaster

FAQ

Why test listeners directly rather than using @SpringBootTest?
Directly testing listeners is significantly faster because it avoids loading the full Spring application context, enabling quicker feedback loops.
Can this approach test asynchronous event listeners?
It can verify the logic within the listener method itself, but it does not test the actual asynchronous threading or executor configuration.
How do I verify the contents of a custom event?
Use the Mockito ArgumentCaptor class to intercept the event passed to the publisher, then perform assertions on the object's properties.

How it compares

Unlike integration tests that require a full context, this skill provides focused, high-speed verification of individual event-handling logic, preventing the overhead of unnecessary bean wiring.

Source & trust

⭐ 282 starsπŸ“„ MITπŸ•’ Updated 2026-06-15
πŸ“„ Full skill instructions β€” original source: giuseppe-trisciuoglio/developer-kit
# Unit Testing Application Events

Test Spring ApplicationEvent publishers and event listeners using JUnit 5. Verify event publishing, listener execution, and event propagation without full context startup.

## When to Use This Skill

Use this skill when:
- Testing ApplicationEventPublisher event publishing
- Testing @EventListener method invocation
- Verifying event listener logic and side effects
- Testing event propagation through listeners
- Want fast event-driven architecture tests
- Testing both synchronous and asynchronous event handling

## Setup: Event Testing

### 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")
}


## Basic Pattern: Event Publishing and Listening

### Custom Event and Publisher

// Custom application event
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 that publishes events
@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 user = new User(name, email);
User savedUser = userRepository.save(user);

eventPublisher.publishEvent(new UserCreatedEvent(this, savedUser));

return savedUser;
}
}

// Unit test
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

@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", "[email protected]");
when(userRepository.save(any(User.class))).thenReturn(newUser);

ArgumentCaptor<UserCreatedEvent> eventCaptor = ArgumentCaptor.forClass(UserCreatedEvent.class);

userService.createUser("Alice", "[email protected]");

verify(eventPublisher).publishEvent(eventCaptor.capture());

UserCreatedEvent capturedEvent = eventCaptor.getValue();
assertThat(capturedEvent.getUser()).isEqualTo(newUser);
}
}


## Testing Event Listeners

### @EventListener Annotation

// Event listener
@Component
public class UserEventListener {

private final EmailService emailService;

public UserEventListener(EmailService emailService) {
this.emailService = emailService;
}

@EventListener
public void onUserCreated(UserCreatedEvent event) {
User user = event.getUser();
emailService.sendWelcomeEmail(user.getEmail());
}
}

// Unit test for listener
class UserEventListenerTest {

@Test
void shouldSendWelcomeEmailWhenUserCreated() {
EmailService emailService = mock(EmailService.class);
UserEventListener listener = new UserEventListener(emailService);

User newUser = new User(1L, "Alice", "[email protected]");
UserCreatedEvent event = new UserCreatedEvent(this, newUser);

listener.onUserCreated(event);

verify(emailService).sendWelcomeEmail("[email protected]");
}

@Test
void shouldNotThrowExceptionWhenEmailServiceFails() {
EmailService emailService = mock(EmailService.class);
doThrow(new RuntimeException("Email service down"))
.when(emailService).sendWelcomeEmail(any());

UserEventListener listener = new UserEventListener(emailService);
User newUser = new User(1L, "Alice", "[email protected]");
UserCreatedEvent event = new UserCreatedEvent(this, newUser);

// Should handle exception gracefully
assertThatCode(() -> listener.onUserCreated(event))
.doesNotThrowAnyException();
}
}


## Testing Multiple Listeners

### Event Propagation

class UserCreatedEvent extends ApplicationEvent {
private final User user;
private final List<String> notifications = new ArrayList<>();

public UserCreatedEvent(Object source, User user) {
super(source);
this.user = user;
}

public void addNotification(String notification) {
notifications.add(notification);
}

public List<String> getNotifications() {
return notifications;
}
}

class MultiListenerTest {

@Test
void shouldNotifyMultipleListenersSequentially() {
EmailService emailService = mock(EmailService.class);
NotificationService notificationService = mock(NotificationService.class);
AnalyticsService analyticsService = mock(AnalyticsService.class);

UserEventListener emailListener = new UserEventListener(emailService);
UserEventListener notificationListener = new UserEventListener(notificationService);
UserEventListener analyticsListener = new UserEventListener(analyticsService);

User user = new User(1L, "Alice", "[email protected]");
UserCreatedEvent event = new UserCreatedEvent(this, user);

emailListener.onUserCreated(event);
notificationListener.onUserCreated(event);
analyticsListener.onUserCreated(event);

verify(emailService).send(any());
verify(notificationService).notify(any());
verify(analyticsService).track(any());
}
}


## Testing Conditional Event Listeners

### @EventListener with Condition

@Component
public class ConditionalEventListener {

@EventListener(condition = "#event.user.age > 18")
public void onAdultUserCreated(UserCreatedEvent event) {
// Handle adult user
}
}

class ConditionalListenerTest {

@Test
void shouldProcessEventWhenConditionMatches() {
// Test logic for matching condition
}

@Test
void shouldSkipEventWhenConditionDoesNotMatch() {
// Test logic for non-matching condition
}
}


## Testing Async Event Listeners

### @Async with @EventListener

@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", "[email protected]");
UserCreatedEvent event = new UserCreatedEvent(this, user);

listener.onUserCreatedAsync(event);

// Event processed asynchronously
Thread.sleep(100); // Wait for async completion
verify(slowService).processUser(user);
}
}


## Best Practices

- **Mock ApplicationEventPublisher** in unit tests
- **Capture published events** using ArgumentCaptor
- **Test listener side effects** explicitly
- **Test error handling** in listeners
- **Keep event listeners focused** on single responsibility
- **Verify event data integrity** when capturing
- **Test both sync and async** event processing

## Common Pitfalls

- Testing actual event publishing without mocking publisher
- Not verifying listener invocation
- Not capturing event details
- Testing listener registration instead of logic
- Not handling listener exceptions

## Troubleshooting

**Event not being captured**: Verify ArgumentCaptor type matches event class.

**Listener not invoked**: Ensure event is actually published and listener is registered.

**Async listener timing issues**: Use Thread.sleep() or Awaitility to wait for completion.

## References

- [Spring ApplicationEvent](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/ApplicationEvent.html)
- [Spring ApplicationEventPublisher](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/ApplicationEventPublisher.html)
- [@EventListener Documentation](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/event/EventListener.html)

How to Use This Skill Unit

Option A: Project-Specific (Recommended)

  1. Click "Download" above
  2. In your project, create the directory: .agent/skills/unit-test-application-events/
  3. Save the file as SKILL.md
  4. The agent will automatically discover the skill based on its description.

Option B: Global Installation (All Agents)

Save the file to these locations to make it available across all projects:

  • Claude Code: ~/.claude/skills/giuseppe-trisciuoglio/developer-kit/unit-test-application-events/SKILL.md
  • Cursor: ~/.cursor/skills/giuseppe-trisciuoglio/developer-kit/unit-test-application-events/SKILL.md
  • Antigravity: ~/.gemini/antigravity/skills/giuseppe-trisciuoglio/developer-kit/unit-test-application-events/SKILL.md

πŸš€ Install with CLI:
npx skills add giuseppe-trisciuoglio/developer-kit

Read the Master Guide: Mastering Agent Skills β†’

Recommended Rules

View more rules β†’

Recommended Workflows

View more workflows β†’

Recommended MCP Servers

View more MCP servers β†’

Take It Further

Maximize your productivity with these powerful resources

πŸ“‹

Define Your Standards

Set up coding standards to ensure this workflow produces consistent, high-quality results.

Browse Rules Library
πŸ“–

Master Workflows

Learn how to create custom workflows, use Turbo Mode, and build your automation library.

Complete Guide

How to use this Skill in Claude Code & Cursor

For Claude Code (CLI)

To use this skill in Claude Code, copy the rule content into your project's custom instructions or follow our Add-Skill CLI guide. This ensures Claude follows your standards during every code generation.

For Cursor & Windsurf

For Cursor or Windsurf, individual skills are best used in the "Rules for AI" section. This specific unit helps the agent avoid testing & quality assurance issues, leading to cleaner, more efficient code.

Why the skill format matters: the standardized Agent Skills format lets your AI agent load detailed instructions only when they are relevant, keeping your prompt clean while improving results.

Source & attribution

This skill is categorized under Testing & Quality Assurance and is published by Giuseppe Trisciuoglio, maintained in giuseppe-trisciuoglio/developer-kit.

← Browse All Agent Skills
Sponsored AI assistant. Recommendations may be paid.