关于 unit-test-application-events
面向 Spring 开发者的测试技能,使用 JUnit 5 和 Mockito 验证应用事件发布者与监听器。重点是在不启动完整 Spring 上下文的情况下编写隔离单元测试,包括异步处理器。
主要功能
- 01模拟事件发布者
- 02捕获并检查事件载荷
- 03隔离测试监听器
- 04验证异步处理器
- 05覆盖监听器错误路径
适用场景
- 01检查已发布事件的类型和字段
- 02使用模拟依赖测试监听器副作用
- 03验证异步事件处理
- 04测试事件处理中的错误情况
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ giuseppe-trisciuoglio/ developer-kit --skill unit-test-application-events安装后在 Claude Code 中输入/unit-test-application-events确认已加载。
详细介绍
单元测试应用事件
概述
提供可操作的模式,用于使用 JUnit 5 和 Mockito 测试 Spring ApplicationEvent 发布器和 @EventListener 消费者,无需启动完整的 Spring 上下文。
适用场景
- 为事件发布器或监听器编写单元测试
- 验证发布的事件是否包含正确的负载
- 测试
@EventListener方法的调用及其副作用 - 测试事件在多个监听器之间的传播
- 验证异步事件处理(
@Async+@EventListener) - 在服务测试中模拟
ApplicationEventPublisher
说明
- 添加测试依赖项:
spring-boot-starter、JUnit 5、Mockito、AssertJ - 模拟 ApplicationEventPublisher:在被测服务的发布器字段上使用
@Mock - 使用 ArgumentCaptor 捕获事件:通过
ArgumentCaptor.forClass(EventType.class)检查已发布的负载 - 验证监听器副作用:直接调用监听器,并使用模拟依赖项
- 测试异步处理器:使用
Thread.sleep()或 Awaitility,然后断言异步操作已被调用 - 添加验证检查点:
- 捕获事件后,先确认
eventCaptor.getValue()不为 null,再断言字段 - 如果未调用监听器,验证是否使用正确的事件类型调用了
publishEvent() - 如果异步断言失败,延长等待时间,并检查执行器线程池是否已饱和
- 捕获事件后,先确认
- 覆盖错误场景:断言监听器能够妥善处理异常
示例
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")
}
自定义事件和发布器测试
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;
}
}
事件发布单元测试
@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());
assertThat(eventCaptor.getValue().getUser()).isEqualTo(newUser);
}
}
监听器直接测试
@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", "[email protected]");
listener.onUserCreated(new UserCreatedEvent(this, user));
verify(emailService).sendWelcomeEmail("[email protected]");
}
@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", "[email protected]");
assertThatCode(() -> listener.onUserCreated(new UserCreatedEvent(this, user)))
.doesNotThrowAnyException();
}
}
异步监听器测试
@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]");
listener.onUserCreatedAsync(new UserCreatedEvent(this, user));
Thread.sleep(200); // checkpoint: allow async executor to run
verify(slowService).processUser(user);
}
}
最佳实践
- 模拟
ApplicationEventPublisher——单元测试中绝不要让它向真实上下文发布事件 - 使用
ArgumentCaptor捕获事件,并断言字段级相等,而不仅仅是类型 - 隔离测试监听器:使用模拟依赖项构造监听器,并直接调用处理方法
- 覆盖错误路径:监听器不得向发布器传播异常
- 异步监听器:优先使用 Awaitility,而不是
Thread.sleep(),以实现确定性等待 - 保持事件不可变且可序列化——如果事件会跨 JVM 边界传递,则两者都要测试
限制和警告
- 不要测试 Spring 自身的事件基础设施——应专注于业务逻辑和事件负载
@Async需要@EnableAsync——即使测试中的异步代理未正确配置,使用 Thread.sleep 的测试仍可能通过;请改用 mock 验证- Spring 不保证监听器的顺序——除非添加
@Order,否则不要编写依赖执行顺序的测试 - 避免在 CI 环境中使用
Thread.sleep()——负载较高时会导致测试不稳定;请改用 Awaitility 的.atMost()代码块 - 跨 JVM 边界传递的事件需要序列化测试——远程监听器中的字段为 null,通常意味着缺少
Serializable
参考资料
评论
支持 Markdown