异步代码测试

异步代码测试

异步代码(如多线程任务、回调函数、消息队列消费等)的单元测试面临时序不确定性和线程隔离等挑战。本章通过具体示例解析如何利用 Mockito 和辅助工具(如 Awaitility)可靠地验证异步逻辑。


1. 基础异步验证:verify() + timeout()

Mockito 的 timeout() 方法允许在指定时间内等待异步调用完成。

示例场景

测试一个异步任务处理器 AsyncTaskProcessor,其在后台线程中执行任务并回调结果:

public class AsyncTaskProcessor {
    private final ExecutorService executor = Executors.newSingleThreadExecutor();
    private final TaskCallback callback;

    public AsyncTaskProcessor(TaskCallback callback) {
        this.callback = callback;
    }

    public void processAsync(String taskData) {
        executor.submit(() -> {
            // 模拟耗时处理
            try {
                Thread.sleep(100);
                callback.onSuccess("Processed: " + taskData);
            } catch (InterruptedException e) {
                callback.onFailure(e);
            }
        });
    }

    public interface TaskCallback {
        void onSuccess(String result);
        void onFailure(Throwable error);
    }
}
测试代码
@ExtendWith(MockitoExtension.class)
class AsyncTaskProcessorTest {

    @Mock
    private AsyncTaskProcessor.TaskCallback mockCallback;

    @InjectMocks
    private AsyncTaskProcessor processor;

    @Test
    void processAsync_ShouldInvokeOnSuccess() {
        // 触发异步任务
        processor.processAsync("test-data");

        // 验证:在 500ms 内 onSuccess 被调用,且参数匹配
        verify(mockCallback, timeout(500))
            .onSuccess(contains("Processed: test-data"));

        // 验证 onFailure 未被调用
        verify(mockCallback, never()).onFailure(any());
    }
}

关键点解析

  • timeout(500):等待最多 500ms,若期间 onSuccess 被调用则通过,否则失败。
  • 精确断言:结合 contains() 匹配器验证结果内容。
  • 清理资源:实际项目中需关闭线程池(示例省略)。

2. 高级异步控制:Awaitility 库

Awaitility 提供更灵活的等待条件,避免硬编码 Thread.sleep,增强测试可读性。

示例场景

测试一个订单状态更新服务,异步修改数据库并发送消息:

public class OrderService {
    private final OrderRepository repository;
    private final MessageProducer messageProducer;

    public void updateOrderStatusAsync(String orderId, String status) {
        CompletableFuture.runAsync(() -> {
            repository.updateStatus(orderId, status);
            messageProducer.send("order.updated", orderId);
        });
    }
}
测试代码
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderRepository mockRepo;

    @Mock
    private MessageProducer mockProducer;

    @InjectMocks
    private OrderService orderService;

    @Test
    void updateOrderStatusAsync_ShouldUpdateAndSendMessage() {
        // 触发异步操作
        orderService.updateOrderStatusAsync("ORDER_123", "SHIPPED");

        // 使用 Awaitility 等待两个操作完成
        await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> {
            verify(mockRepo).updateStatus(eq("ORDER_123"), eq("SHIPPED"));
            verify(mockProducer).send(eq("order.updated"), eq("ORDER_123"));
        });
    }
}

关键点解析

  • 依赖添加:需在 pom.xml 引入 Awaitility:
    <dependency>
        <groupId>org.awaitility</groupId>
        <artifactId>awaitility</artifactId>
        <version>4.2.0</version>
        <scope>test</scope>
    </dependency>
    
  • 复合条件untilAsserted 内可包含多个断言,确保所有异步操作完成。
  • 超时控制atMost(1, TimeUnit.SECONDS) 设置最大等待时间。

3. 回调参数捕获与验证

结合 ArgumentCaptor 和异步等待机制,深入验证回调参数。

示例场景

测试一个支付网关客户端,异步执行支付并回调结果:

public class PaymentGatewayClient {
    public void payAsync(BigDecimal amount, PaymentCallback callback) {
        new Thread(() -> {
            boolean success = externalPaymentService.process(amount);
            if (success) {
                callback.onSuccess(UUID.randomUUID().toString());
            } else {
                callback.onFailure("Payment failed");
            }
        }).start();
    }

    public interface PaymentCallback {
        void onSuccess(String transactionId);
        void onFailure(String error);
    }
}
测试代码
@ExtendWith(MockitoExtension.class)
class PaymentGatewayClientTest {

    @Mock
    private PaymentGatewayClient.PaymentCallback mockCallback;

    private PaymentGatewayClient client = new PaymentGatewayClient();

    @Captor
    private ArgumentCaptor<String> transactionIdCaptor;

    @Test
    void payAsync_ShouldCallOnSuccessWithValidId() {
        // 触发支付
        client.payAsync(new BigDecimal("100.00"), mockCallback);

        // 等待回调完成,捕获参数
        verify(mockCallback, timeout(1000))
            .onSuccess(transactionIdCaptor.capture());

        // 验证交易ID格式
        String transactionId = transactionIdCaptor.getValue();
        assertThat(transactionId)
            .isNotNull()
            .matches("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}");
    }
}

关键点解析

  • 精确超时timeout(1000) 等待 1 秒,足够后台线程完成。
  • 正则验证:使用 AssertJ 的 matches() 确保 UUID 格式正确。
  • 线程安全ArgumentCaptor 本身线程安全,可直接用于异步场景。

4. 处理异步异常场景

模拟依赖服务异常,验证错误处理逻辑。

示例场景

测试当数据库访问超时时,服务是否重试:

public class RetryableService {
    private final DatabaseClient dbClient;
    private int retries = 3;

    public void fetchDataWithRetry() {
        for (int i = 0; i < retries; i++) {
            try {
                dbClient.fetchData();
                return;
            } catch (DatabaseTimeoutException e) {
                if (i == retries - 1) throw e;
            }
        }
    }
}
测试代码
@ExtendWith(MockitoExtension.class)
class RetryableServiceTest {

    @Mock
    private DatabaseClient mockDbClient;

    @InjectMocks
    private RetryableService retryableService;

    @Test
    void fetchDataWithRetry_ShouldRetry3TimesOnTimeout() {
        // 模拟连续超时
        doThrow(DatabaseTimeoutException.class)
            .when(mockDbClient).fetchData();

        // 执行并验证最终抛出异常
        assertThrows(DatabaseTimeoutException.class,
            () -> retryableService.fetchDataWithRetry());

        // 验证重试次数
        verify(mockDbClient, times(3)).fetchData();
    }
}

关键点解析

  • 连续异常模拟doThrow() 默认每次调用都抛出异常。
  • 精准重试验证times(3) 确保重试逻辑正确。
  • 同步转异步:若 fetchDataWithRetry 为异步方法,需结合 Awaitility 等待。

总结

场景工具关键技巧
简单异步回调verify() + timeout()明确超时时间,结合参数匹配器验证结果
复杂多步骤异步Awaitility使用 untilAsserted() 等待复合条件
异步参数验证ArgumentCaptor线程安全捕获参数,结合正则或复杂断言
异常重试逻辑doThrow() + times()模拟连续异常,验证重试次数和最终状态

通过合理选择工具链和验证策略,可有效应对异步代码的不可预测性,构建稳定可靠的单元测试。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值