Appearance
Spring Testing 中的 @MockitoBean 和 @MockitoSpyBean 详解 🎯
引言:为什么需要测试中的 Mock 和 Spy?
在现代软件开发中,单元测试是保证代码质量的重要手段。但在实际测试过程中,我们经常遇到这样的困扰:
测试中的常见痛点
- 依赖复杂:被测试的类依赖于数据库、外部服务或其他复杂组件
- 环境限制:测试环境无法访问真实的外部资源
- 测试隔离:希望测试只关注当前类的逻辑,而不受依赖项影响
- 性能考虑:真实依赖可能执行缓慢,影响测试效率
这就是 Mock 和 Spy 技术诞生的背景。Spring Framework 通过 @MockitoBean
和 @MockitoSpyBean
注解,让我们能够优雅地在测试中替换或包装 Spring 容器中的 Bean。
核心概念理解
Mock vs Spy:本质区别
核心区别
- Mock(模拟):完全替换原始对象,所有行为都需要预先定义
- Spy(间谍):包装原始对象,可以选择性地拦截某些方法调用,其他方法仍调用真实实现
@MockitoBean 深度解析
基本原理与使用场景
@MockitoBean
采用 REPLACE_OR_CREATE
策略,它会:
- 在 Spring ApplicationContext 中查找匹配的 Bean
- 如果找到,则用 Mockito Mock 对象替换
- 如果未找到,则创建一个新的 Mock Bean
实战示例:电商订单服务测试
kotlin
@SpringBootTest
class OrderControllerTest {
@Autowired
private lateinit var orderController: OrderController
@Test
fun `should create order successfully`() {
// ❌ 问题:这会调用真实的支付服务和库存服务
// 可能导致:
// 1. 测试依赖外部服务
// 2. 可能产生真实的支付行为
// 3. 测试不稳定
val result = orderController.createOrder(
CreateOrderRequest("user123", "product456", 2)
)
assertThat(result.status).isEqualTo("SUCCESS")
}
}
kotlin
@SpringBootTest
class OrderControllerTest {
@Autowired
private lateinit var orderController: OrderController
@MockitoBean
private lateinit var paymentService: PaymentService
@MockitoBean
private lateinit var inventoryService: InventoryService
@Test
fun `should create order successfully`() {
// ✅ 预设 Mock 行为
given(inventoryService.checkStock("product456", 2))
.willReturn(true)
given(paymentService.processPayment(any()))
.willReturn(PaymentResult.success("payment123"))
val result = orderController.createOrder(
CreateOrderRequest("user123", "product456", 2)
)
// ✅ 验证结果和交互
assertThat(result.status).isEqualTo("SUCCESS")
verify(inventoryService).reserveStock("product456", 2)
verify(paymentService).processPayment(any())
}
}
高级用法:按名称指定和类型级别配置
复杂场景示例:多支付方式处理
kotlin
// 业务场景:系统支持多种支付方式
@Service
class OrderService(
private val alipayService: PaymentService,
private val wechatPayService: PaymentService,
private val creditCardService: PaymentService
) {
fun processOrder(order: Order, paymentType: PaymentType): OrderResult {
val paymentService = when (paymentType) {
PaymentType.ALIPAY -> alipayService
PaymentType.WECHAT -> wechatPayService
PaymentType.CREDIT_CARD -> creditCardService
}
return paymentService.pay(order.amount)
}
}
// 测试类:需要精确控制不同的支付服务
@SpringBootTest
class OrderServiceTest {
@Autowired
private lateinit var orderService: OrderService
// 按名称指定要 Mock 的 Bean
@MockitoBean("alipayService")
private lateinit var alipayService: PaymentService
@MockitoBean("wechatPayService")
private lateinit var wechatPayService: PaymentService
@MockitoBean("creditCardService")
private lateinit var creditCardService: PaymentService
@Test
fun `should use correct payment service for alipay`() {
// 只为支付宝服务设置 Mock 行为
given(alipayService.pay(any()))
.willReturn(PaymentResult.success("alipay_123"))
val result = orderService.processOrder(
Order(amount = BigDecimal("100.00")),
PaymentType.ALIPAY
)
// 验证只调用了支付宝服务
verify(alipayService).pay(any())
verify(wechatPayService, never()).pay(any())
verify(creditCardService, never()).pay(any())
}
}
组合注解:提高测试复用性
kotlin
// 创建自定义组合注解,统一管理常用的 Mock 配置
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MockitoBean(types = [OrderService::class, UserService::class])
@MockitoBean(name = "emailNotificationService", types = [NotificationService::class])
annotation class CommonServiceMocks
// 在多个测试类中复用
@SpringBootTest
@CommonServiceMocks
class OrderControllerTest {
@Autowired private lateinit var orderService: OrderService
@Autowired private lateinit var userService: UserService
@Autowired private lateinit var emailNotificationService: NotificationService
@Test
fun `should handle order creation workflow`() {
// 所有依赖的服务都已经被 Mock,可以专注于测试逻辑
given(userService.findById(any())).willReturn(mockUser)
given(orderService.createOrder(any())).willReturn(mockOrder)
// 测试代码...
}
}
@MockitoSpyBean 深度解析
Spy 的独特价值
Spy 最大的价值在于部分模拟:你可以保留对象的大部分真实行为,只对特定方法进行拦截或验证。
实战示例:缓存服务测试
kotlin
@Service
class ProductService(
private val productRepository: ProductRepository,
private val cacheService: CacheService
) {
fun getProduct(id: String): Product? {
// 先尝试从缓存获取
val cached = cacheService.get("product:$id", Product::class.java)
if (cached != null) {
return cached
}
// 缓存未命中,从数据库查询
val product = productRepository.findById(id)
if (product != null) {
// 将结果缓存
cacheService.put("product:$id", product, Duration.ofMinutes(30))
}
return product
}
}
@SpringBootTest
class ProductServiceTest {
@Autowired
private lateinit var productService: ProductService
@MockitoBean
private lateinit var productRepository: ProductRepository
@MockitoSpyBean
private lateinit var cacheService: CacheService
@Test
fun `should cache product after first query`() {
val productId = "product123"
val mockProduct = Product(productId, "Test Product", BigDecimal("99.99"))
// Mock 数据库查询
given(productRepository.findById(productId)).willReturn(mockProduct)
// 第一次查询 - 应该从数据库获取并缓存
val result1 = productService.getProduct(productId)
// 验证缓存被调用
verify(cacheService).get("product:$productId", Product::class.java)
verify(cacheService).put("product:$productId", mockProduct, Duration.ofMinutes(30))
// 第二次查询 - 应该从缓存获取
val result2 = productService.getProduct(productId)
// 验证数据库只被查询了一次
verify(productRepository, times(1)).findById(productId)
assertThat(result1).isEqualTo(mockProduct)
assertThat(result2).isEqualTo(mockProduct)
}
@Test
fun `should handle cache failure gracefully`() {
val productId = "product456"
val mockProduct = Product(productId, "Another Product", BigDecimal("149.99"))
// 模拟缓存获取失败
doThrow(RuntimeException("Cache unavailable"))
.when(cacheService).get(any(), any<Class<*>>())
given(productRepository.findById(productId)).willReturn(mockProduct)
val result = productService.getProduct(productId)
// 即使缓存失败,也应该能从数据库获取数据
assertThat(result).isEqualTo(mockProduct)
verify(productRepository).findById(productId)
}
}
Spy 的最佳使用场景
- 缓存测试:验证缓存的读写行为,同时保持缓存逻辑的真实性
- 日志和监控:验证关键操作是否正确记录日志或发送监控指标
- 事件发布:确认业务操作后是否正确发布了相应的事件
- 部分功能替换:只替换某些方法的行为,保持其他功能不变
高级特性与最佳实践
1. 上下文层次结构中的使用
kotlin
// 在复杂的上下文层次结构中精确控制 Mock 的作用范围
@SpringBootTest
@ContextHierarchy(
ContextConfiguration(name = "parent", classes = [ParentConfig::class]),
ContextConfiguration(name = "child", classes = [ChildConfig::class])
)
class HierarchyTest {
// 只在 child 上下文中生效
@MockitoBean(contextName = "child")
private lateinit var childService: ChildService
// 在所有上下文中生效(默认行为)
@MockitoBean
private lateinit var globalService: GlobalService
}
2. 字段可见性的灵活性
字段可见性说明
@MockitoBean
和 @MockitoSpyBean
字段可以是任何可见性:
public
、protected
、包私有或private
- 根据项目的编码规范和需求选择合适的可见性
kotlin
@SpringBootTest
class FlexibilityTest {
@MockitoBean
private lateinit var privateService: PrivateService // ✅ 私有字段
@MockitoBean
protected lateinit var protectedService: ProtectedService // ✅ 受保护字段
@MockitoBean
lateinit var publicService: PublicService // ✅ 公共字段
}
3. 性能优化:避免不必要的上下文重建
性能陷阱
不一致的字段命名会导致 Spring 创建多个 ApplicationContext,严重影响测试性能!
kotlin
// 测试类 A
class OrderServiceTestA {
@MockitoBean
private lateinit var paymentSvc: PaymentService // 字段名:paymentSvc
}
// 测试类 B
class OrderServiceTestB {
@MockitoBean
private lateinit var paymentService: PaymentService // 字段名:paymentService
}
// 结果:Spring 会创建两个不同的 ApplicationContext!
kotlin
// 测试类 A
class OrderServiceTestA {
@MockitoBean
private lateinit var paymentService: PaymentService
}
// 测试类 B
class OrderServiceTestB {
@MockitoBean
private lateinit var paymentService: PaymentService
}
// 结果:复用同一个 ApplicationContext,测试运行更快!
实际业务场景综合示例
让我们通过一个完整的电商订单处理系统来展示这些注解的实际应用:
完整的业务场景示例
kotlin
// 业务服务类
@Service
class OrderProcessingService(
private val inventoryService: InventoryService,
private val paymentService: PaymentService,
private val notificationService: NotificationService,
private val auditService: AuditService
) {
@Transactional
fun processOrder(orderRequest: OrderRequest): OrderResult {
// 1. 检查库存
if (!inventoryService.checkAvailability(orderRequest.productId, orderRequest.quantity)) {
return OrderResult.failed("库存不足")
}
// 2. 预留库存
inventoryService.reserveStock(orderRequest.productId, orderRequest.quantity)
try {
// 3. 处理支付
val paymentResult = paymentService.processPayment(
PaymentRequest(orderRequest.userId, orderRequest.amount)
)
if (!paymentResult.isSuccess) {
// 支付失败,释放库存
inventoryService.releaseStock(orderRequest.productId, orderRequest.quantity)
return OrderResult.failed("支付失败: ${paymentResult.errorMessage}")
}
// 4. 创建订单
val order = createOrder(orderRequest, paymentResult.transactionId)
// 5. 发送通知(异步)
notificationService.sendOrderConfirmation(order)
// 6. 记录审计日志
auditService.logOrderCreated(order)
return OrderResult.success(order)
} catch (e: Exception) {
// 异常处理,释放库存
inventoryService.releaseStock(orderRequest.productId, orderRequest.quantity)
throw e
}
}
private fun createOrder(request: OrderRequest, transactionId: String): Order {
return Order(
id = UUID.randomUUID().toString(),
userId = request.userId,
productId = request.productId,
quantity = request.quantity,
amount = request.amount,
transactionId = transactionId,
status = OrderStatus.CONFIRMED,
createdAt = Instant.now()
)
}
}
// 综合测试类
@SpringBootTest
class OrderProcessingServiceTest {
@Autowired
private lateinit var orderProcessingService: OrderProcessingService
// Mock 外部依赖服务
@MockitoBean
private lateinit var inventoryService: InventoryService
@MockitoBean
private lateinit var paymentService: PaymentService
@MockitoBean
private lateinit var notificationService: NotificationService
// Spy 审计服务,保持真实行为但验证调用
@MockitoSpyBean
private lateinit var auditService: AuditService
@Test
fun `should process order successfully when all conditions are met`() {
// Given: 准备测试数据
val orderRequest = OrderRequest(
userId = "user123",
productId = "product456",
quantity = 2,
amount = BigDecimal("199.98")
)
// 设置 Mock 行为
given(inventoryService.checkAvailability("product456", 2))
.willReturn(true)
given(paymentService.processPayment(any()))
.willReturn(PaymentResult.success("txn_123456"))
// When: 执行业务操作
val result = orderProcessingService.processOrder(orderRequest)
// Then: 验证结果和交互
assertThat(result.isSuccess).isTrue()
assertThat(result.order?.transactionId).isEqualTo("txn_123456")
// 验证服务调用顺序和参数
val inOrder = inOrder(inventoryService, paymentService, notificationService, auditService)
inOrder.verify(inventoryService).checkAvailability("product456", 2)
inOrder.verify(inventoryService).reserveStock("product456", 2)
inOrder.verify(paymentService).processPayment(any())
inOrder.verify(notificationService).sendOrderConfirmation(any())
inOrder.verify(auditService).logOrderCreated(any())
}
@Test
fun `should release stock when payment fails`() {
// Given: 库存充足但支付失败的场景
val orderRequest = OrderRequest(
userId = "user123",
productId = "product456",
quantity = 1,
amount = BigDecimal("99.99")
)
given(inventoryService.checkAvailability("product456", 1))
.willReturn(true)
given(paymentService.processPayment(any()))
.willReturn(PaymentResult.failed("信用卡余额不足"))
// When
val result = orderProcessingService.processOrder(orderRequest)
// Then: 验证失败处理逻辑
assertThat(result.isSuccess).isFalse()
assertThat(result.errorMessage).contains("支付失败")
// 验证库存被正确释放
verify(inventoryService).reserveStock("product456", 1)
verify(inventoryService).releaseStock("product456", 1)
// 验证失败时不发送通知和审计日志
verify(notificationService, never()).sendOrderConfirmation(any())
verify(auditService, never()).logOrderCreated(any())
}
@Test
fun `should handle inventory shortage`() {
// Given: 库存不足的场景
val orderRequest = OrderRequest(
userId = "user123",
productId = "product789",
quantity = 10,
amount = BigDecimal("999.90")
)
given(inventoryService.checkAvailability("product789", 10))
.willReturn(false)
// When
val result = orderProcessingService.processOrder(orderRequest)
// Then
assertThat(result.isSuccess).isFalse()
assertThat(result.errorMessage).isEqualTo("库存不足")
// 验证库存不足时不进行后续操作
verify(inventoryService, never()).reserveStock(any(), any())
verify(paymentService, never()).processPayment(any())
verify(notificationService, never()).sendOrderConfirmation(any())
verify(auditService, never()).logOrderCreated(any())
}
}
总结与最佳实践 🎉
核心价值回顾
- 测试隔离性 - 通过 Mock/Spy 隔离外部依赖,让测试更加稳定可靠
- 开发效率 - 无需搭建复杂的测试环境,快速验证业务逻辑
- 精确控制 - 可以模拟各种边界条件和异常场景
- 行为验证 - 不仅验证结果,还能验证方法调用的顺序和参数
选择指南
何时使用 @MockitoBean?
- 需要完全控制依赖对象的行为
- 测试复杂的业务逻辑分支
- 模拟外部服务调用
- 验证特定的方法调用和参数
何时使用 @MockitoSpyBean?
- 需要保留大部分真实行为,只拦截特定方法
- 验证缓存、日志、事件发布等横切关注点
- 测试装饰器模式或代理模式的实现
- 需要部分模拟的集成测试场景
注意事项
重要提醒
- 保持字段命名一致性,避免创建多余的 ApplicationContext
- 只对单例 Bean 使用,非单例 Bean 会抛出异常
- 在
@ContextHierarchy
中使用时,注意指定正确的contextName
- 合理使用组合注解,提高测试代码的复用性
通过掌握 @MockitoBean
和 @MockitoSpyBean
,你将能够编写更加健壮、高效的单元测试,让代码质量得到显著提升! ✅