题目
设计可测试的支付服务并处理外部依赖和异常场景
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
Mock与Stub的区分, 依赖注入设计, 异常处理策略, 测试金字塔实践
快速回答
在测试支付服务时需解决:
- 使用Stub模拟支付网关的固定响应(如成功/失败)
- 使用Mock验证支付回调逻辑是否被正确触发
- 通过依赖注入解耦外部服务(如PaymentGateway)
- 设计异常测试场景:网络超时、无效凭证、余额不足等
- 遵循测试金字塔原则:单元测试为主,少量集成测试
问题场景
假设有一个支付服务类 PaymentProcessor,依赖外部支付网关 PaymentGateway 和回调服务 NotificationService。要求:
- 处理支付时需记录交易日志
- 支付成功后发送回调通知
- 处理支付网关的各类异常(如超时、拒付)
核心解决方案
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:简易实现(如内存数据库)