Mock与Stub
一、核心概念与差异对比
特性 | Mock | Stub |
---|---|---|
核心目的 | 验证对象间的交互行为 | 提供预定义的固定响应 |
验证重点 | 方法调用次数、参数、顺序 | 不关注调用过程,只关注结果 |
行为模拟 | 可编程的智能模拟 | 静态的简单响应 |
适用场景 | 验证协作关系 | 隔离依赖、提供固定数据 |
复杂性 | 较高(需要设置预期行为) | 较低(简单硬编码响应) |
架构决策点:选择Mock还是Stub取决于测试目标 - 验证交互行为用Mock,替换依赖项用Stub
二、架构设计中的分层应用
测试金字塔中的定位
- 单元测试层:Mock主导(验证类间协作)
- 集成测试层:Stub主导(模拟外部依赖)
- 端到端测试:真实依赖(不使用模拟)
Spring测试架构
// 典型Spring测试分层
@SpringBootTest // 集成测试
class IntegrationTest {@MockBean DependencyService service; // MockBean替代真实依赖@Test void testIntegration() {// 使用Stub提供预设数据given(service.getData()).willReturn(new Data("stub value"));}
}@ExtendWith(MockitoExtension.class) // 单元测试
class UnitTest {@Mock Dependency dependency;@InjectMocks ServiceUnderTest service;@Test void testBehavior() {// 设置Mock行为并验证交互when(dependency.process(any())).thenReturn(true);service.execute();verify(dependency, times(1)).process(any());}
}
三、Spring生态中的实现详解
1. Stub实现方案
场景:为支付网关提供测试响应
// 定义支付网关接口
public interface PaymentGateway {PaymentResult charge(Order order);
}// 创建Stub实现
public class StubPaymentGateway implements PaymentGateway {private final PaymentResult fixedResult;public StubPaymentGateway(PaymentResult result) {this.fixedResult = result;}@Overridepublic PaymentResult charge(Order order) {// 静态响应,不包含业务逻辑return fixedResult;}
}// 测试中使用
@Test
void testOrderProcessingWithStub() {// 创建带成功响应的StubPaymentGateway stubGateway = new StubPaymentGateway(new PaymentResult("SUCCESS", "TX-123"));OrderService service = new OrderService(stubGateway);Order order = new Order(100.0);service.process(order);assertThat(order.getStatus()).isEqualTo(OrderStatus.PAID);
}
2. Mock实现方案(Mockito框架)
场景:验证库存服务调用
@ExtendWith(MockitoExtension.class)
class InventoryServiceTest {@Mock InventoryRepository mockRepo; // 创建Mock对象@InjectMocks InventoryService inventoryService; // 注入被测试对象@Testvoid shouldUpdateInventoryWhenOrderPaid() {// 准备测试数据Order order = new Order("order-1");order.addItem("prod-001", 2);// 设置Mock行为when(mockRepo.findByProductId("prod-001")).thenReturn(new Inventory("prod-001", 10));// 执行测试inventoryService.updateInventory(order);// 验证交互行为verify(mockRepo, times(1)).findByProductId("prod-001");verify(mockRepo, times(1)).save(argThat(inv -> inv.getQuantity() == 8)); // 验证保存参数// 验证未发生的方法调用verify(mockRepo, never()).delete(any());}
}
四、高级模式与应用场景
1. 部分Mock(Spy对象)
场景:测试复杂服务中的特定方法
@Spy // 部分Mock:真实对象+可Mock特定方法
private EmailService emailService;@Test
void testOrderNotification() {// 模拟发送邮件方法doNothing().when(emailService).sendConfirmation(any());orderService.processOrder(order);verify(emailService).sendConfirmation(order);
}
2. Spring Cloud Contract(契约测试)
架构方案:跨服务边界的Stub管理
提供方(定义契约):
// payment-contract.groovy
Contract.make {request {method POST()url '/payments'body([orderId: anyUuid(),amount: anyPositiveDouble()])}response {status 201body([status: "SUCCESS",transactionId: regex('[0-9a-f]{8}-[0-9a-f]{4}')])}
}
消费方(测试中使用Stub):
@SpringBootTest
@AutoConfigureStubRunner(ids = "com.example:payment-service:+:stubs")
class OrderServiceContractTest {@Testvoid shouldProcessPayment() {PaymentResult result = paymentClient.charge(new PaymentRequest(...));assertThat(result.getStatus()).isEqualTo("SUCCESS");}
}
五、架构师的最佳实践建议
-
分层使用策略
- 单元测试:80% Mock + 20% Stub
- 集成测试:20% Mock + 80% Stub
- 契约测试:100% Stub(基于提供方契约)
-
性能优化
// 重用Mock对象提升测试速度 @MockitoSettings(strictness = Strictness.LENIENT) public class ReusableMocksTest {@Mock static DatabaseConnection sharedConnection; }
-
反模式警示
- ❌ 过度验证:
verify(mock, times(3)).trivialMethod()
- ❌ Mock传递:
when(mockA.call()).thenReturn(mockB)
- ❌ 脆弱的Stub:硬编码不合理的测试数据
- ❌ 过度验证:
-
现代替代方案
- 虚拟服务(Testcontainers):代替Stub提供更真实的模拟
@Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
- 代码生成Stub(OpenAPI Generator):基于API规范自动生成
六、架构决策树
graph TDA[需要测试什么?] A --> B{验证对象交互?}B -->|是| C[使用Mock]B -->|否| D{需要控制依赖行为?}D -->|是| E[使用Stub]D -->|否| F[使用真实对象]C --> G{需要部分真实行为?}G -->|是| H[使用Spy]G -->|否| I[使用纯Mock]E --> J{跨服务边界?}J -->|是| K[使用契约测试Stub]J -->|否| L[使用简单Stub实现]
作为系统架构师,我建议:在保证测试质量的前提下选择最简单的模拟方案。Mock适合验证内部协作,Stub适合隔离外部依赖,契约测试Stub则是微服务架构的必备工具。正确使用这些技术可以构建快速、可靠且维护成本低的测试体系。