You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 23:17:14