如何为CompletableFuture的cancel方法编写JUnit测试及超时异常验证
如何Mock cancel方法并编写单元测试
针对你提供的cancel方法,我们可以通过Mockito、JUnit 5结合反射(或轻量修改原代码)来实现单元测试,覆盖正常执行、超时抛出异常、ApiException捕获三个核心场景。
前提准备
确保测试依赖包含JUnit 5、Mockito核心及JUnit扩展:
<dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.9.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>4.11.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>4.11.0</version> <scope>test</scope> </dependency> </dependencies>
原代码优化(可选但推荐)
为了方便测试超时场景,建议将超时参数、执行器提取为可配置项,避免硬编码:
import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; public class TaskService { private static final long CANCEL_TIMEOUT = 3; private static final TimeUnit CANCEL_TIMEOUT_UNIT = TimeUnit.MINUTES; private final Executor executor; // 生产环境传入ForkJoinPool.commonPool(),测试用自定义Executor public TaskService(Executor executor) { this.executor = executor; } private void cancel() throws TimeoutException, ExecutionException, InterruptedException { try { CompletableFuture.supplyAsync(() -> { try { cancelTask(); } catch (ApiException ex) { Logger.error("Api exception while cancelling a tasks"); } return null; }, executor).get(CANCEL_TIMEOUT, CANCEL_TIMEOUT_UNIT); } catch (InterruptedException | ExecutionException | TimeoutException e) { throw e; } } private void cancelTask() throws ApiException { // 实际任务取消逻辑 } }
单元测试实现
场景1:cancelTask正常执行,方法无异常
测试cancelTask在超时时间内完成时,cancel方法正常结束:
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.TimeoutException; import static org.mockito.Mockito.doNothing; @ExtendWith(MockitoExtension.class) public class TaskServiceTest { @Spy @InjectMocks private TaskService taskService = new TaskService(Executors.newSingleThreadExecutor()); @Test void cancel_WhenTaskCompletesInTime_NoExceptionThrown() throws TimeoutException, ExecutionException, InterruptedException { // Mock private方法cancelTask,模拟正常执行 doNothing().when(taskService).cancelTask(); // 反射调用private的cancel方法 java.lang.reflect.Method cancelMethod = TaskService.class.getDeclaredMethod("cancel"); cancelMethod.setAccessible(true); cancelMethod.invoke(taskService); } }
场景2:cancelTask执行超时,抛出TimeoutException
通过反射修改超时参数为1秒,模拟cancelTask执行超时,验证方法抛出TimeoutException:
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.doAnswer; @ExtendWith(MockitoExtension.class) public class TaskServiceTest { private final ExecutorService testExecutor = Executors.newSingleThreadExecutor(); @Spy @InjectMocks private TaskService taskService = new TaskService(testExecutor); @Test void cancel_WhenTaskTimesOut_ThrowsTimeoutException() { try { // 反射修改超时参数为1秒,避免测试等待3分钟 java.lang.reflect.Field timeoutField = TaskService.class.getDeclaredField("CANCEL_TIMEOUT"); timeoutField.setAccessible(true); timeoutField.setLong(taskService, 1); java.lang.reflect.Field timeoutUnitField = TaskService.class.getDeclaredField("CANCEL_TIMEOUT_UNIT"); timeoutUnitField.setAccessible(true); timeoutUnitField.set(taskService, TimeUnit.SECONDS); } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException(e); } // Mock cancelTask,模拟执行2秒(超过1秒超时) doAnswer(invocation -> { Thread.sleep(2000); return null; }).when(taskService).cancelTask(); // 验证方法抛出TimeoutException assertThrows(TimeoutException.class, () -> { java.lang.reflect.Method cancelMethod = TaskService.class.getDeclaredMethod("cancel"); cancelMethod.setAccessible(true); cancelMethod.invoke(taskService); }); testExecutor.shutdownNow(); } }
场景3:cancelTask抛出ApiException,方法正常结束
模拟cancelTask抛出ApiException,验证方法捕获异常并正常结束:
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.TimeoutException; import static org.mockito.Mockito.doThrow; @ExtendWith(MockitoExtension.class) public class TaskServiceTest { @Spy @InjectMocks private TaskService taskService = new TaskService(Executors.newSingleThreadExecutor()); @Test void cancel_WhenTaskThrowsApiException_NoExceptionThrown() throws TimeoutException, ExecutionException, InterruptedException { // Mock cancelTask抛出ApiException doThrow(new ApiException("Test API error")).when(taskService).cancelTask(); // 调用cancel方法,预期无异常抛出 java.lang.reflect.Method cancelMethod = TaskService.class.getDeclaredMethod("cancel"); cancelMethod.setAccessible(true); cancelMethod.invoke(taskService); // 可选:验证错误日志是否打印(需引入SLF4J Testing依赖) // TestLogger logger = TestLoggerFactory.getTestLogger(TaskService.class); // logger.verifyError("Api exception while cancelling a tasks"); } }
补充说明
- 若不想修改原代码注入Executor,可使用PowerMock mock
CompletableFuture.supplyAsync方法,直接返回超时的Future,但配置相对复杂。 - 针对private方法的测试,反射是轻量方案;若需更便捷的private方法Mock,可结合PowerMock的
@PrepareForTest注解。
内容的提问来源于stack exchange,提问作者Harish
相关产品推荐
相关产品推荐

