一、核心概念與差異對比
特性 | 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則是微服務架構的必備工具。正確使用這些技術可以構建快速、可靠且維護成本低的測試體系。