目录
-
- @Mock和@Spy两者区别
- 使用方法
-
- 核心概念速览
- [1. `@Mock` 的使用方法](#1.
@Mock的使用方法) -
- a) 基础使用:定义方法返回值 基础使用:定义方法返回值)
- b) 让方法抛出异常 让方法抛出异常)
- c) 验证交互行为(Verification) 验证交互行为(Verification))
- [2. `@Spy` 的使用方法](#2.
@Spy的使用方法) -
- a) 基础使用:只修改特定方法 基础使用:只修改特定方法)
- b) Spy 真实方法,但验证调用 Spy 真实方法,但验证调用)
- c) 覆盖(Mock)Spy 的某个方法 覆盖(Mock)Spy 的某个方法)
- 总结与选择
- [@RunWith(MockitoJUnitRunner.class) 和 @ExtendWith(MockitoExtension.class)](#@RunWith(MockitoJUnitRunner.class) 和 @ExtendWith(MockitoExtension.class))
- [@Spy @Mock和@SpyBean @MockBean的区别](#@Spy @Mock和@SpyBean @MockBean的区别)
@Mock和@Spy两者区别
在单元测试中(以 Mockito 框架为例),@Spy、@Mock 和 @InjectMocks 注解的使用有明确的限制:
1. @Mock
-
可以用于接口或具体类(包括抽象类)。
-
因为 Mockito 可以模拟任何类型(接口或实现类),生成一个完全模拟的对象(所有方法默认被 stub,返回空值或自定义值)。
-
示例:
java@Mock private UserDao userDao; // 可以是接口或实现类
2. @Spy
-
必须用于具体类(非抽象)或已实例化的对象,不能用于接口或抽象类。
-
原因:
@Spy需要包装一个真实对象(通过调用构造函数或现有实例),而接口和抽象类无法直接实例化。 -
示例:
java@Spy private UserServiceImpl userService; // 必须是具体类 // 错误:不能用于接口 // @Spy // private UserService userService;
3. @InjectMocks
-
必须用于具体类(非抽象),不能用于接口或抽象类。
-
原因:
@InjectMocks会创建该类的真实实例(通过构造函数或 setter 注入依赖),而接口和抽象类无法实例化。 -
示例:
java@InjectMocks private UserServiceImpl userService; // 必须是具体实现类 // 错误:不能用于接口 // @InjectMocks // private UserService userService;
总结表格:
| 注解 | 能否用于接口? | 能否用于抽象类? | 能否用于具体类? | 说明 |
|---|---|---|---|---|
@Mock |
✅ 是 | ✅ 是 | ✅ 是 | 生成完全模拟对象 |
@Spy |
❌ 否 | ❌ 否 | ✅ 是 | 包装真实对象(需可实例化) |
@InjectMocks |
❌ 否 | ❌ 否 | ✅ 是 | 创建真实实例并注入依赖 |
关键原则:
- Mock 可以模拟任何类型(包括接口),因为它不依赖实际实现。
- Spy 和 InjectMocks 必须基于可实例化的具体类,因为它们需要操作真实对象。
使用方法
以下是 @Spy 和 @Mock 在单元测试中的核心使用方法和区别,以最流行的 Mockito 框架为例。
核心概念速览
| 特性 | @Mock |
@Spy |
|---|---|---|
| 本质 | 创建一个完全虚拟的模拟对象 | 创建一个真实对象 的包装器(部分模拟) |
| 默认行为 | 所有方法默认返回"空值"(如 null, 0, false 等) |
所有方法默认调用真实对象的实现 |
| 使用场景 | 隔离测试,完全控制依赖的行为和输出 | 验证对象部分 行为,或只修改少数方法 |
1. @Mock 的使用方法
@Mock 用于创建一个完全模拟的对象。你必须 为其方法显式定义行为(Stubbing),否则它们将返回默认的空值。
a) 基础使用:定义方法返回值
java
// 1. 创建Mock
@Mock
private UserDao userDaoMock;
// 2. 定义方法行为(Stubbing)
@Test
public void testWithMock() {
// 当调用 findById(1L) 时,返回一个预设的User对象
when(userDaoMock.findById(1L)).thenReturn(new User(1L, "小荣"));
// 当调用任何Long类型的参数时,都返回另一个预设对象
when(userDaoMock.findById(any(Long.class))).thenReturn(new User(999L, "Default User"));
// 执行测试...
User result = userService.getUser(1L); // 内部调用了 userDaoMock.findById(1L)
assertEquals("小荣", result.getName());
}
b) 让方法抛出异常
java
@Test
public void testMockThrowException() {
// 当调用 deleteById(1L) 时,抛出一个运行时异常
when(userDaoMock.deleteById(1L)).thenThrow(new RuntimeException("Database error"));
// 验证服务层是否正确处理了异常
assertThrows(RuntimeException.class, () -> userService.deleteUser(1L));
}
c) 验证交互行为(Verification)
java
@Test
public void testMockVerification() {
userService.updateUser(new User(1L, "New Name"));
// 验证 userDaoMock 的 update 方法被精确地调用了一次
verify(userDaoMock, times(1)).update(any(User.class));
// 验证某个方法从未被调用
verify(userDaoMock, never()).deleteById(any());
}
2. @Spy 的使用方法
@Spy 用于包装一个真实对象。默认情况下,它会调用对象的真实方法。你只需要为你想要改变的方法定义行为。
a) 基础使用:只修改特定方法
java
// 1. 创建Spy(注意:必须是具体类)
@Spy
private EmailService emailServiceSpy; // 假设这是一个真实的发邮件服务
@Test
public void testWithSpy() {
// 我们不想在测试中真的发邮件,所以模拟掉 sendEmail 方法
doNothing().when(emailServiceSpy).sendEmail(anyString()); // 让 sendEmail 方法什么都不做
userService.sendWelcomeEmail("user@example.com"); // 内部会调用 emailServiceSpy.sendEmail(...)
// 验证发送邮件的逻辑是否被触发(但实际并未发送)
verify(emailServiceSpy).sendEmail("user@example.com");
}
b) Spy 真实方法,但验证调用
java
@Test
public void testSpyRealMethod() {
// 没有为 calculateDiscount 定义行为,所以会调用真实方法
double discount = userService.calculateDiscount(100.0);
// 验证真实方法被调用了一次
verify(emailServiceSpy).calculateDiscount(100.0);
assertEquals(90.0, discount, 0.0); // 假设真实逻辑是打9折
}
c) 覆盖(Mock)Spy 的某个方法
java
@Test
public void testSpyOverrideMethod() {
// 覆盖真实方法,让其返回一个固定值
when(emailServiceSpy.calculateDiscount(100.0)).thenReturn(80.0);
double discount = userService.calculateDiscount(100.0);
assertEquals(80.0, discount, 0.0); // 现在返回的是模拟值,不是真实值
}
注意 :对 Spy 对象使用 when(...).thenReturn(...) 语法时,真实方法会先被调用一次 。如果不想调用真实方法,应使用 doReturn(...).when(...) 语法:
java
// 推荐用法:避免先调用真实方法
doReturn(80.0).when(emailServiceSpy).calculateDiscount(100.0);
总结与选择
-
用
@Mock当:- 你需要一个完全可控的、轻量的假对象。
- 该对象的所有方法你都想自定义。
- 该对象很难构造或依赖外部资源(如数据库、网络)。
-
用
@Spy当:- 你有一个真实的、可用的对象。
- 你只想验证这个对象的某些方法是否被调用(或调用次数)。
- 你只想修改这个对象的一两个方法,其他方法保持真实行为。
- 你想进行部分模拟。
@RunWith(MockitoJUnitRunner.class) 和 @ExtendWith(MockitoExtension.class)
好的,这两个注解是 Mockito 测试框架中用于初始化和注入 @Mock、@Spy 等注解的"启动器"。它们的作用是相同的,但分别用于不同的单元测试框架。
核心解释
| 注解 | 适用的测试框架 | 作用 |
|---|---|---|
@RunWith(MockitoJUnitRunner.class) |
JUnit 4 | 在测试开始前,自动初始化所有带有 @Mock, @Spy, @InjectMocks 等注解的字段。 |
@ExtendWith(MockitoExtension.class) |
JUnit 5 (Jupiter) | 功能同上,是 JUnit 5 的新扩展模型,取代了 JUnit 4 的 @RunWith。 |
简单来说,它们的作用是:自动帮你完成 MockitoAnnotations.openMocks(this) 这句代码的工作。
必须得加吗?
不一定,但有它更方便、更推荐。
你有三种方式来初始化 Mockito 注解:
1. 使用 @ExtendWith 或 @RunWith (推荐 👍)
这是最简洁、最现代的方式。你只需要在测试类上添加一个注解,框架就会自动处理所有初始化。
- JUnit 5 示例:
java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class) // 关键注解
public class MyServiceTest {
@Mock
private UserRepository userRepository; // 自动被初始化
@InjectMocks
private MyService myService; // 自动被创建,并将上面的mock注入进去
@Test
public void testSomething() {
// 直接使用已经初始化好的 mock 和被测对象
when(userRepository.findById(1L)).thenReturn(new User(...));
// ... 测试逻辑
}
}
- JUnit 4 示例:
java
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class) // 关键注解
public class MyServiceTest {
// ... 内容同上
}
2. 手动初始化 (传统方式)
你可以在 @Before (JUnit 4) 或 @BeforeEach (JUnit 5) 方法中手动调用初始化。这种方式不需要 在类上加 @ExtendWith 或 @RunWith。
java
import org.junit.jupiter.api.BeforeEach;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class MyServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private MyService myService;
@BeforeEach // 在每个测试方法执行前运行
public void setUp() {
MockitoAnnotations.openMocks(this); // 手动初始化注解
}
@Test
public void testSomething() {
// ... 测试逻辑
}
}
3. 完全不初始化 (错误方式 ❌)
如果你既不使用 @ExtendWith/@RunWith,也不手动调用 openMocks(this),那么你的 @Mock 和 @InjectMocks 字段将会是 null,导致测试运行时抛出 NullPointerException。
java
public class MyServiceTest {
@Mock // 这个注解未被处理,userRepository 为 null
private UserRepository userRepository;
@Test
public void testSomething() {
when(userRepository.findById(1L)).thenReturn(...); // 这里会抛出 NPE!
}
}
总结与建议
| 情况 | 建议 |
|---|---|
| 使用 JUnit 5 | 必须使用 @ExtendWith(MockitoExtension.class)。这是标准且最简洁的做法。 |
| 使用 JUnit 4 | 必须使用 @RunWith(MockitoJUnitRunner.class)。 |
| 不想用类注解 | 可以手动调用 MockitoAnnotations.openMocks(this),但更繁琐。 |
| 什么都不做 | 绝对不行,测试会失败。 |
结论:为了代码的简洁性和可读性,强烈建议你根据使用的 JUnit 版本,在测试类上加上对应的注解。 这已经不是"好不好"的问题,而是现代单元测试的标准写法。
@Spy @Mock和@SpyBean @MockBean的区别
好的,这是一个非常核心的测试概念。@Mock/@Spy 和 @MockBean/@SpyBean 的关键区别在于它们的应用场景和所属框架。
简单来说:
@Mock和@Spy是 Mockito 框架的核心注解 ,用于普通的单元测试(Unit Test),隔离性最强,速度最快。@MockBean和@SpyBean是 Spring Boot Test 提供的注解 ,用于集成测试(Integration Test),需要启动 Spring 容器。
下面我们通过一个详细的对比来深入理解。
为了让您更直观地理解它们的区别,我为您准备了一个对比表格:
| 特性 | @Mock / @Spy (Mockito) |
@MockBean / @SpyBean (Spring Boot Test) |
|---|---|---|
| 所属框架 | Mockito | Spring Boot Test (内部整合了 Mockito) |
| 测试类型 | 单元测试 (Unit Test) | 集成测试 (Integration Test) |
| 是否需要Spring容器 | 否。测试轻量级,运行速度快。 | 是。需要启动完整的或部分的Spring应用上下文。 |
| 主要目的 | 隔离被测类,用模拟对象替换其所有依赖。 | 在Spring容器中,替换掉某一个特定的Bean,其他Bean保持不变。 |
| 集成方式 | 与 JUnit 通过 @ExtendWith(MockitoExtension.class) 集成。 |
与 Spring Test 通过 @SpringBootTest 等注解集成。 |
| 默认行为 | @Mock: 所有方法都是虚拟的,默认返回null或空集合。 @Spy: 包装真实对象,默认调用真实方法。 |
同上,但因为存在于容器中,所以能被@Autowired注入到其他Bean里。 |
深入解析与代码示例
1. @Mock 和 @Spy (纯Mockito单元测试)
这种场景下,没有Spring容器 。你测试的类是自己手动创建(或通过@InjectMocks注入)的,它的所有依赖都是你手动提供的Mock或Spy对象。
@Mock: 创建一个所有方法都被模拟的"假"对象。你需要用when().thenReturn()来定义它的行为。如果没定义,方法默认返回null或空值。@Spy: 创建一个"部分真实"的对象,它包装了一个真实实例。默认会调用真实对象的方法,除非你显式地模拟(stub)某个特定方法。
示例:测试一个不涉及Spring的简单服务类
java
// 一个简单的服务,不涉及Spring注解
public class PaymentService {
private final FraudCheckService fraudCheckService;
private final TransactionRepository transactionRepository;
// 通过构造函数注入依赖
public PaymentService(FraudCheckService fcs, TransactionRepository tr) {
this.fraudCheckService = fcs;
this.transactionRepository = tr;
}
public boolean processPayment(Payment payment) {
if (fraudCheckService.isFraudulent(payment)) { // 依赖1
return false;
}
return transactionRepository.save(payment); // 依赖2
}
}
// 单元测试类
@ExtendWith(MockitoExtension.class) // 使用Mockito 扩展
class PaymentServiceUnitTest {
@Mock // 创建一个完全的Mock,我们根本不关心它的真实逻辑
private FraudCheckService fraudCheckServiceMock;
@Spy // 创建一个Spy,我们可能想部分模拟,部分调用真实方法
private TransactionRepository transactionRepositorySpy;
@InjectMocks // 创建被测类实例,并自动将上面的@Mock和@Spy注入进去
private PaymentService paymentService;
@Test
void shouldDeclineFraudulentPayment() {
// 1. 准备数据
Payment fraudulentPayment = new Payment(...);
// 2. 定义Mock的行为:当调用isFraudulent时,返回true
when(fraudCheckServiceMock.isFraudulent(fraudulentPayment)).thenReturn(true);
// 3. 执行测试方法
boolean result = paymentService.processPayment(fraudulentPayment);
// 4. 验证结果和行为
assertFalse(result); // 断言结果为false
// 验证因为被判定为欺诈,save方法没有被调用
verify(transactionRepositorySpy, never()).save(any());
}
}
2. @MockBean 和 @SpyBean (Spring集成测试)
这种场景下,你需要启动Spring容器 。你的测试类上有 @SpringBootTest 等注解。容器中已经有很多真实的Bean,但你希望将其中的某一个或某几个替换为Mock或Spy,以便进行隔离测试。
@MockBean: 在Spring应用上下文中添加一个Mockito Mock,以替换任何现有的相同类型的Bean。如果不存在该类型的Bean,它会添加一个。@SpyBean: 在Spring应用上下文中包装一个现有的、真实的Bean,创建一个Spy。如果找不到该Bean,会抛出异常。
示例:测试一个Spring的Controller
java
// Controller,其依赖由Spring自动注入
@RestController
public class UserController {
@Autowired
private UserService userService; // 这个Bean存在于Spring容器中
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
}
// 集成测试类
@SpringBootTest // 启动整个Spring容器
@AutoConfigureMockMvc // 自动配置MockMvc用于模拟HTTP请求
class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@MockBean // 关键!用Mock替换掉Spring容器中真实的UserService Bean
private UserService userServiceMock;
@Test
void shouldReturnUser() throws Exception {
// 0. 准备一个模拟用户
User mockUser = new User("John Doe");
// 1. 定义被Mock的Bean的行为
when(userServiceMock.findById(1L)).thenReturn(Optional.of(mockUser));
// 2. 执行模拟HTTP请求并验证
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John Doe"));
// 3. 可以验证Mock Bean的交互
verify(userServiceMock).findById(1L);
}
}
如何选择?
- 如果你在做纯粹的单元测试 ,测试的类不涉及Spring容器管理(如:普通的工具类、领域模型、非
@Component的Service),使用@Mock和@Spy。这是首选,因为速度极快。 - 如果你在测试Spring的组件 (如:
@Controller,@Service,@Repository)并且需要启动应用上下文,但又想模拟其中的某个依赖,使用@MockBean和@SpyBean。
记住一个简单的法则:看你的测试类上有没有 @SpringBootTest 。如果有,就用 @MockBean/@SpyBean;如果没有,就用 @Mock/@Spy。