侧边栏壁纸
博主头像
colo

欲买桂花同载酒

  • 累计撰写 1823 篇文章
  • 累计收到 0 条评论

设计可测试的支付服务并处理外部依赖和异常场景

2025-12-14 / 0 评论 / 4 阅读

题目

设计可测试的支付服务并处理外部依赖和异常场景

信息

  • 类型:问答
  • 难度:⭐⭐⭐

考点

Mock与Stub的区分, 依赖注入设计, 异常处理策略, 测试金字塔实践

快速回答

在测试支付服务时需解决:

  • 使用Stub模拟支付网关的固定响应(如成功/失败)
  • 使用Mock验证支付回调逻辑是否被正确触发
  • 通过依赖注入解耦外部服务(如PaymentGateway)
  • 设计异常测试场景:网络超时、无效凭证、余额不足等
  • 遵循测试金字塔原则:单元测试为主,少量集成测试
## 解析

问题场景

假设有一个支付服务类 PaymentProcessor,依赖外部支付网关 PaymentGateway 和回调服务 NotificationService。要求:

  1. 处理支付时需记录交易日志
  2. 支付成功后发送回调通知
  3. 处理支付网关的各类异常(如超时、拒付)

核心解决方案

1. 依赖注入设计

public class PaymentProcessor {
    private final PaymentGateway gateway;
    private final NotificationService notifier;

    // 依赖通过构造函数注入
    public PaymentProcessor(PaymentGateway gateway, NotificationService notifier) {
        this.gateway = gateway;
        this.notifier = notifier;
    }

    public PaymentResult processPayment(PaymentRequest request) {
        try {
            GatewayResponse response = gateway.charge(request);

            if (response.isSuccess()) {
                notifier.sendSuccessNotification(request.getUserId()); // 需验证此调用
                return PaymentResult.success(response.getTransactionId());
            } else {
                return PaymentResult.failed(response.getErrorCode());
            }
        } catch (GatewayTimeoutException e) {
            // 特定异常处理
            return PaymentResult.failed("TIMEOUT");
        }
    }
}

2. 测试策略与工具(以Java/Mockito为例)

Stub应用:模拟支付网关响应
// 测试支付成功场景
@Test
void whenPaymentSuccess_thenReturnSuccessResult() {
    // 1. 创建Stub(模拟固定响应)
    PaymentGateway stubGateway = request -> 
        new GatewayResponse("TX123", true, "");

    // 2. 创建Mock(用于验证行为)
    NotificationService mockNotifier = mock(NotificationService.class);

    PaymentProcessor processor = new PaymentProcessor(stubGateway, mockNotifier);
    PaymentResult result = processor.processPayment(new PaymentRequest(100.0));

    // 验证状态
    assertTrue(result.isSuccess());

    // 验证交互:确保通知被调用1次
    verify(mockNotifier, times(1)).sendSuccessNotification(anyString());
}
Mock应用:验证异常处理逻辑
// 测试支付网关超时
@Test
void whenGatewayTimeout_thenReturnTimeoutError() {
    // 1. 创建会抛出异常的Stub
    PaymentGateway stubGateway = request -> { 
        throw new GatewayTimeoutException("Connection timeout");
    };

    PaymentProcessor processor = new PaymentProcessor(stubGateway, mock(NotificationService.class));
    PaymentResult result = processor.processPayment(new PaymentRequest(100.0));

    assertEquals("TIMEOUT", result.getErrorCode());
    // 验证通知服务不应被调用
    verifyNoInteractions(mockNotifier);
}

关键区分:Mock vs Stub

类型目的验证重点示例场景
Stub提供预设响应状态验证(结果值)模拟支付网关返回成功/失败
Mock验证交互行为行为验证(方法调用)检查回调服务是否被正确触发

最佳实践

  • 分层测试
    • 单元测试:Mock/Stub所有外部依赖(快速验证业务逻辑)
    • 集成测试:仅Stub最外层依赖(如真实数据库+Stub支付网关)
  • 异常覆盖
    • 模拟所有可能的异常(网络错误、无效输入、服务不可用)
    • 使用Mockito的when().thenThrow()模拟异常
  • 避免过度Mock
    • Mock仅用于有副作用的交互(如发送通知、写入日志)
    • 纯计算逻辑无需Mock

常见错误

  • 混淆Mock与Stub:在只需返回预设值时使用Mock验证行为
  • 过度指定验证:如验证具体参数值而非类型,导致测试脆弱
  • 忽略异常路径:只测试成功场景,未覆盖失败流程
  • 测试实现细节:验证内部方法调用而非最终结果

扩展知识

  • Spy:部分真实对象+部分Mock,用于监控已有实例
  • 契约测试:确保Stub与真实服务的一致性(如Pact框架)
  • 测试替身类型
    • Dummy:空对象用于填充参数
    • Fake:简易实现(如内存数据库)