在Java单元测试领域,Mockito无疑是模拟依赖行为的标杆框架。然而,许多开发者在实际使用中,对@Mock和@InjectMocks这两个核心注解的职责和区别理解模糊,导致测试代码要么过于冗长,要么注入失败。Java Mockito @Mock 和 @InjectMocks 区别的核心价值在于,它明确了测试中“创建模拟对象”与“构建被测对象并自动注入模拟依赖”的两种不同职责,通过合理的分工协作,让我们能够以最简洁的方式构建出依赖关系清晰、隔离性完备的测试环境。正确理解并运用这对注解,是编写高效、可维护单元测试的关键。
一、 回归本质:为什么要模拟与注入?

在深入注解之前,必须理解单元测试的核心原则:隔离性。当我们测试一个服务类(如OrderService)时,它通常依赖其他组件(如PaymentGateway、InventoryRepository)。为了专注测试OrderService自身的逻辑,我们需要将这些依赖“模拟”(Mock)——即创建其替身,并控制其行为(如“当调用charge方法时,返回成功”)。
传统方式需要手动创建模拟对象,并通过构造函数或setter方法将其注入被测对象,步骤繁琐。Mockito通过@Mock和@InjectMocks这两个注解,将此过程声明化、自动化。在“鳄鱼java”网站的《测试驱动开发实战》系列中,我们反复强调:理解这两个注解的分工,是掌握Mockito高效用法的第一步。
二、 @Mock注解:专注创建“演员替身”
@Mock 注解的唯一职责是:告诉Mockito框架,请为我创建一个指定类型的模拟对象(Mock Object)。这个模拟对象会完全替代真实的依赖,其所有方法在默认情况下都不执行真实逻辑,而是返回“空”值(如null、0、false或空集合)。
import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import static org.mockito.Mockito.*;@RunWith(MockitoJUnitRunner.class) // 必须使用Mockito测试运行器 public class OrderServiceTest {
// 声明一个PaymentGateway的模拟对象 @Mock private PaymentGateway mockPaymentGateway; // 声明一个InventoryRepository的模拟对象 @Mock private InventoryRepository mockInventoryRepo; @Test public void testPlaceOrder() { // 在测试方法中,mockPaymentGateway和mockInventoryRepo已经是可用的模拟对象 // 我们可以为它们定义行为 when(mockPaymentGateway.charge(anyDouble())).thenReturn(true); when(mockInventoryRepo.isInStock("ITEM_001")).thenReturn(true); // ... 后续测试逻辑 }
}
关键点:
1. @Mock注解标记的是依赖对象,即被测对象需要使用的协作组件。
2. 被@Mock标记的字段,会在测试初始化阶段(由MockitoJUnitRunner或MockitoAnnotations.openMocks(this))自动实例化为Mockito代理对象。
3. 它的存在不涉及任何注入逻辑,仅仅负责“创建替身”。
三、 @InjectMocks注解:担任“导演”,组装测试场景
@InjectMocks 注解的职责则更进一步:告诉Mockito,请实例化这个类(通常是被测类),并尝试将当前测试上下文中的相关模拟对象(@Mock对象)自动注入到其中。 它是构建完整测试场景的“导演”。
@RunWith(MockitoJUnitRunner.class) public class OrderServiceTest {@Mock private PaymentGateway mockPaymentGateway; @Mock private InventoryRepository mockInventoryRepo; // @InjectMocks标记的是被测对象(System Under Test, SUT) @InjectMocks private OrderService orderService; // Mockito将创建真实的OrderService实例 @Test public void testPlaceOrder_Success() { // 1. 布置场景(Define Behavior) when(mockInventoryRepo.isInStock("ITEM_001")).thenReturn(true); when(mockPaymentGateway.charge(100.0)).thenReturn(true); // 2. 执行动作(Call Method) OrderResult result = orderService.placeOrder("ITEM_001", 100.0); // 3. 验证结果(Assert & Verify) assertTrue(result.isSuccess()); // 验证模拟对象的交互是否按预期发生 verify(mockInventoryRepo).isInStock("ITEM_001"); verify(mockPaymentGateway).charge(100.0); }
}
关键点:
1. @InjectMocks标记的是被测对象,即我们想要测试其行为的类。
2. Mockito会尝试通过以下方式(按顺序)注入依赖:
- 构造函数注入(优先):寻找匹配参数最多的构造函数。
- Setter方法注入:查找方法名符合setter规范的公共方法。
- 字段注入(直接赋值):直接给字段赋值(即使是private字段)。
3. 如果找不到合适的注入方式,依赖可能保持为null,这通常意味着你的类设计或测试设置需要调整。
这就是Java Mockito @Mock 和 @InjectMocks 区别的直观体现:一个负责造“零件”(模拟依赖),一个负责用这些零件“组装”主机(被测对象)。
四、 深度对比:区别、联系与工作原理
| 特性 | @Mock | @InjectMocks |
|---|---|---|
| 核心职责 | 创建并管理模拟对象(替身) | 实例化被测对象并自动注入模拟依赖 |
| 标记对象 | 协作依赖(如Repository, Service, Gateway) | 被测对象(System Under Test, SUT) |
| 注入行为 | 不执行注入,仅提供模拟对象引用 | 主动执行依赖注入(构造函数、setter、字段) |
| 对象状态 | 空实现,所有方法需用when().thenReturn()定义行为 | 真实对象实例,包含部分或全部模拟依赖 |
| 生命周期 | 每个测试方法前都可能被重置(取决于Runner设置) | 每个测试方法前被重新实例化和注入 |
联系与协作:
@InjectMocks 高度依赖于 @Mock。只有在同一测试类中通过@Mock(或@Spy)创建的模拟对象,才会被考虑注入到@InjectMocks标记的对象中。它们是生产者(@Mock)与消费者(@InjectMocks)的关系。
五、 实战进阶:复杂注入场景解析
场景1:构造函数注入(推荐)
如果OrderService只有一个构造函数,Mockito会自动使用它进行注入。
public class OrderService { private final PaymentGateway gateway; private final InventoryRepository repository;// 单一构造函数 public OrderService(PaymentGateway gateway, InventoryRepository repository) { this.gateway = gateway; this.repository = repository; }
} // 测试类中,@InjectMocks会自动调用此构造函数,并传入@Mock对象
场景2:多个构造函数的选择
Mockito会选择参数匹配最多的那个构造函数。如果存在多个相同参数数量的构造函数,行为可能不确定,应避免这种设计。
场景3:Setter与字段注入
当没有构造函数,或者希望通过setter注入时,Mockito会尝试调用setter方法或直接设置字段值。字段名或setter方法名需要与模拟对象的字段名匹配(默认按类型匹配,若同一类型有多个模拟对象,则按名称匹配)。
public class OrderService {
private PaymentGateway paymentGateway; // 字段名与@Mock字段名一致
// setter...
}
在“鳄鱼java”的工程实践中,我们强烈推荐使用构造函数注入,因为它使依赖关系明确且不可变,与Mockito的自动注入机制配合得天衣无缝。
六、 常见陷阱与最佳实践
陷阱1:忘记初始化注解
必须使用@RunWith(MockitoJUnitRunner.class)或在@Before方法中调用MockitoAnnotations.openMocks(this)来激活注解处理。否则,@Mock和@InjectMocks字段将为null。
陷阱2:混淆@InjectMocks与@Spy
@Spy用于部分模拟(包装真实对象),而@InjectMocks用于创建新实例并注入依赖。切勿在同一个字段上同时使用。
陷阱3:依赖注入失败
如果注入后依赖仍为null,检查:1)依赖是否为final字段(Mockito 4+支持);2)是否有多个同类型模拟对象导致混淆;3)构造函数/Setter是否可访问。
最佳实践总结:
- 明确分工:
@Mock造依赖,@InjectMocks组主角。 - 优先构造函数:设计被测类时,使用单一的、包含所有必需依赖的构造函数。
- 保持简单:避免在
@InjectMocks对象中混合真实对象和模拟对象,除非使用@Spy且有明确理由。 - 及时验证:使用
verify()确认模拟对象的交互符合预期,这是Mockito相对于单纯Stub框架的独特优势。 - 重置状态:如果使用
openMocks,记得在@After中调用close释放资源。
总结与思考
深刻理解Java Mockito @Mock 和 @InjectMocks 区别,本质上是掌握了一种高效构建测试环境的思维模式。它将测试设置从繁琐的手动组装,提升到声明式、自动化协作的新层次。
请审视你的测试代码:是否存在手动new OrderService(mock1, mock2)的冗长写法?是否因为混淆了这两个注解,导致测试时依赖注入总是不成功?当你下次为一个复杂服务编写测试时,不妨先画出依赖图,用@Mock标注所有叶子节点(外部依赖),然后用@InjectMocks标注根部(被测服务),让Mockito自动完成这幅拼图。最终,清晰的测试结构不仅能验证代码的正确性,更能成为系统设计的优秀文档。
版权声明
本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。





