Spring Boot+Cucumber+Mockito场景下,异步事件触发的第三方调用验证失败问题求助
Spring Boot+Cucumber+Mockito场景下,异步事件触发的第三方调用验证失败问题求助
我来帮你分析下这个问题,你遇到的情况其实很典型,核心是异步线程时序和事务提交时机这两个点在搞鬼,咱们一步步来解决:
问题根源拆解
- 异步线程的竞态条件:你用了
@Async注解,事件监听方法会在一个新的后台线程执行。但Cucumber的Then步骤是同步执行的——当你调用verify的时候,后台线程里的sendMessage可能还没跑完,Mockito自然检测不到任何交互,哪怕断点能打到,那只是说明线程被触发了,但执行完成时间晚于验证步骤。 - 事务提交的延迟触发:你的监听方法用了
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT),这意味着只有当触发MyEvent的事务完全提交后,监听方法才会被调用。而Spring Boot测试默认是事务回滚的(即使你加了@Transactional),事务没提交的话,监听方法根本不会启动,这也会导致验证失败。
针对性解决方案
方案1:给Mockito的verify加超时等待(最简单快速)
Mockito支持在verify时指定超时时间,让测试等待异步线程完成执行:
@Then("a message is sent to the ThirdPartySystem") public void messageIsSent() { final ArgumentCaptor<RequestDto> captor = ArgumentCaptor.forClass(RequestDto.class); // 等待1秒,确保异步线程完成调用 verify(thirdPartyClientAdapter, timeout(1000)).sendMessage(captor.capture()); assertThat(captor.getValue().getAmount()).isEqualTo(AMOUNT.toString()); }
这个方案不用改太多代码,适合快速验证问题,但要注意超时时间设置要合理,避免测试不稳定。
方案2:手动控制测试事务提交(解决AFTER_COMMIT触发问题)
如果是事务没提交导致监听方法没执行,你可以用Spring Test提供的TestTransaction类手动提交事务:
import org.springframework.test.context.transaction.TestTransaction; @Given("an event is triggered") public void triggerEvent() { // 这里是你触发MyEvent的代码,比如调用service方法发布事件 myService.publishMyEvent(); // 手动标记事务为提交并结束,触发AFTER_COMMIT阶段的监听 TestTransaction.flagForCommit(); TestTransaction.end(); }
这样事务会立即提交,监听方法会被触发,之后再执行Then步骤的验证就没问题了。
方案3:测试环境禁用异步(彻底避免线程问题)
你可以给异步方法加环境判断,在测试时让它同步执行:
首先修改事件监听方法:
@Async @Profile("!test") // 非test环境才启用异步 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onApplicationEvent(final MyEvent event) { thirdPartyFacade.sendMessage(MyEventMapper.INSTANCE.from(event)); } // 测试环境用同步的监听方法 @Profile("test") @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onApplicationEventSync(final MyEvent event) { thirdPartyFacade.sendMessage(MyEventMapper.INSTANCE.from(event)); }
然后在你的Cucumber测试类里激活test profile:
@Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "my.domain") @CucumberContextConfiguration @SpringBootTest @ActiveProfiles("test") // 启用测试profile public class CucumberTestSuite { @MockitoSpyBean private ThirdPartyClientAdapter thirdPartyClientAdapter; }
这样测试时监听方法是同步执行的,verify就能正常捕获调用了,测试稳定性更高。
方案4:用CountDownLatch手动等待异步线程(最灵活)
如果需要更精确的线程等待控制,可以用CountDownLatch:
首先在测试类里定义一个Latch Bean:
@CucumberContextConfiguration @SpringBootTest public class CucumberTestSuite { @MockitoSpyBean private ThirdPartyClientAdapter thirdPartyClientAdapter; @Bean public CountDownLatch eventProcessingLatch() { return new CountDownLatch(1); } }
然后修改事件监听方法,执行完成后触发Latch:
@Async @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onApplicationEvent(final MyEvent event, CountDownLatch eventProcessingLatch) { try { thirdPartyFacade.sendMessage(MyEventMapper.INSTANCE.from(event)); } finally { // 无论成功失败,都触发Latch eventProcessingLatch.countDown(); } }
最后在Then步骤里等待Latch:
@Then("a message is sent to the ThirdPartySystem") public void messageIsSent(CountDownLatch eventProcessingLatch) throws InterruptedException { // 等待最多1秒,超时则抛出异常 if (!eventProcessingLatch.await(1, TimeUnit.SECONDS)) { throw new AssertionError("异步事件处理超时"); } final ArgumentCaptor<RequestDto> captor = ArgumentCaptor.forClass(RequestDto.class); verify(thirdPartyClientAdapter).sendMessage(captor.capture()); assertThat(captor.getValue().getAmount()).isEqualTo(AMOUNT.toString()); }
这个方案适合复杂场景,能更精准地控制异步流程的等待逻辑。
建议优先尝试的顺序
我建议你先试试方案2,因为你的监听方法依赖AFTER_COMMIT,如果测试事务没提交,异步线程根本不会启动,这可能是核心问题。如果事务提交后还是有问题,再加上方案1的超时等待,应该就能解决了。如果想要更稳定的测试,方案3是长期维护的好选择。
内容来源于stack exchange
相关产品推荐
相关产品推荐

