mcpField
U
unit-test-application-events

unit-test-application-events

github.com/giuseppe-trisciuoglio测试与质量3653.7k 次安装7 周前更新
代码仓库

关于 unit-test-application-events

面向 Spring 开发者的测试技能,使用 JUnit 5 和 Mockito 验证应用事件发布者与监听器。重点是在不启动完整 Spring 上下文的情况下编写隔离单元测试,包括异步处理器。

主要功能

  1. 01模拟事件发布者
  2. 02捕获并检查事件载荷
  3. 03隔离测试监听器
  4. 04验证异步处理器
  5. 05覆盖监听器错误路径

适用场景

  1. 01检查已发布事件的类型和字段
  2. 02使用模拟依赖测试监听器副作用
  3. 03验证异步事件处理
  4. 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

说明

  1. 添加测试依赖项:spring-boot-starter、JUnit 5、Mockito、AssertJ
  2. 模拟 ApplicationEventPublisher:在被测服务的发布器字段上使用 @Mock
  3. 使用 ArgumentCaptor 捕获事件:通过 ArgumentCaptor.forClass(EventType.class) 检查已发布的负载
  4. 验证监听器副作用:直接调用监听器,并使用模拟依赖项
  5. 测试异步处理器:使用 Thread.sleep() 或 Awaitility,然后断言异步操作已被调用
  6. 添加验证检查点:
    • 捕获事件后,先确认 eventCaptor.getValue() 不为 null,再断言字段
    • 如果未调用监听器,验证是否使用正确的事件类型调用了 publishEvent()
    • 如果异步断言失败,延长等待时间,并检查执行器线程池是否已饱和
  7. 覆盖错误场景:断言监听器能够妥善处理异常

示例

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