如何用Mockito测试包含CompletableFuture异步调用的Java方法
如何对包含CompletableFuture.supplyAsync的Java方法进行单元测试?
针对你的ServiceA#methodA方法,我来一步步讲解如何编写可靠的单元测试——核心是解决异步任务的测试稳定性问题,同时验证所有依赖组件的交互逻辑。我们会用到JUnit 5和Mockito这两个常用的测试框架。
第一步:准备测试依赖与测试类结构
首先确保项目中引入了JUnit 5和Mockito的依赖(Maven/Gradle均可)。然后创建测试类,通过@Mock注解Mock所有依赖组件,用@InjectMocks注入待测试的ServiceA实例:
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.ResponseEntity; import java.util.concurrent.CompletableFuture; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class ServiceATest { // Mock所有依赖组件 @Mock private ThreadPoolTaskExecutor taskExecutor; @Mock private ClassA objA; @Mock private ClassB objB; // 注入待测试的ServiceA实例 @InjectMocks private ServiceA serviceA; // 测试用例写在这里... }
第二步:解决异步任务的测试稳定性问题
methodA中使用了自定义线程池taskExecutor执行异步任务,如果直接测试,异步任务可能还没执行完,测试代码就已经走完了,导致验证失败。我们可以通过Mockito让线程池同步执行任务,把异步逻辑变成同步的:
// 在测试用例中添加这段代码,让线程池直接在当前线程执行任务 doAnswer(invocation -> { Runnable runnable = invocation.getArgument(0); runnable.run(); // 同步执行异步任务 return null; }).when(taskExecutor).execute(any(Runnable.class));
这样一来,所有异步逻辑都会在测试线程中同步完成,我们就能准确验证后续的方法调用。
第三步:编写正常流程的测试用例
测试当异步任务正常执行时,所有依赖方法是否被正确调用,以及返回值是否符合预期:
@Test void methodA_WhenAllStepsSucceed_ReturnsOkAndExecutesAsyncFlow() { // 1. 配置线程池同步执行 doAnswer(invocation -> { Runnable runnable = invocation.getArgument(0); runnable.run(); return null; }).when(taskExecutor).execute(any(Runnable.class)); // 2. Mock objA.getData的返回值(替换成你实际的返回类型) Object mockDataResult = new Object(); when(objA.getData(eq(123), any(Object.class))).thenReturn(mockDataResult); // 3. 执行待测试方法 CompletableFuture<ResponseEntity> resultFuture = serviceA.methodA(new Object(), 123); ResponseEntity result = resultFuture.join(); // 4. 验证返回值(你的methodA固定返回OK,所以直接断言) assert result.equals(ResponseEntity.ok().build()); // 5. 验证依赖方法的调用 verify(objA).getData(eq(123), any(Object.class)); // 检查objA的getData是否用正确参数调用 verify(objB).doSomeThing(any(), any()); // 检查objB的doSomeThing是否被调用(可根据实际参数调整匹配器) }
第四步:编写异常流程的测试用例
测试当异步任务抛出异常时,exceptionally分支是否正确处理,以及依赖方法的调用情况:
@Test void methodA_WhenAsyncTaskThrowsException_HandlesErrorAndReturnsOk() { // 1. 同样配置线程池同步执行 doAnswer(invocation -> { Runnable runnable = invocation.getArgument(0); runnable.run(); return null; }).when(taskExecutor).execute(any(Runnable.class)); // 2. Mock objA.getData抛出异常 when(objA.getData(eq(456), any(Object.class))).thenThrow(new RuntimeException("Test error")); // 3. 执行待测试方法 CompletableFuture<ResponseEntity> resultFuture = serviceA.methodA(new Object(), 456); ResponseEntity result = resultFuture.join(); // 4. 验证返回值仍然是OK assert result.equals(ResponseEntity.ok().build()); // 5. 验证objB的doSomeThing没有被调用(因为异常被捕获了) verify(objB, never()).doSomeThing(any(), any()); }
额外注意事项
- 同步方法的测试:
methodA中的validating(obj)和someProcessing(obj, userId)是同步执行的,你可以单独测试这些逻辑,比如模拟validating抛出异常,验证methodA是否会正确处理(比如提前返回或抛出对应异常)。 - 私有方法的验证:如果
convertToSomething是ServiceA的私有方法,不需要单独测试——通过验证objB.doSomeThing的参数是否符合预期,就能间接验证convertToSomething的逻辑是否正确。 - 参数匹配器的准确性:尽量用
eq()匹配具体的参数值,而不是泛用的any(),这样验证结果会更准确,避免误判。
内容的提问来源于stack exchange,提问作者Abhishek Sarda
相关产品推荐
相关产品推荐

