SpringBoot单元测试Mock和Spy

目录

@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

相关推荐
Java程序员威哥3 小时前
SpringBoot2.x与3.x自动配置注册差异深度解析:从原理到迁移实战
java·大数据·开发语言·hive·hadoop·spring boot·后端
shejizuopin3 小时前
基于Spring Boot+小程序的非遗科普平台设计与实现(毕业论文)
spring boot·后端·小程序·毕业设计·论文·毕业论文·非遗科普平台设计与实现
vx_bisheyuange4 小时前
【源码免费送】计算机毕设精选项目:基于SpringBoot的汽车租赁系统的设计与实现
spring boot·汽车·毕业设计·需求分析
浅水壁虎4 小时前
任务调度——XXLJOB3(执行器)
java·服务器·前端·spring boot
小唐同学爱学习5 小时前
短链接修改之写锁
spring boot·redis·后端·mysql
zbguolei6 小时前
Springboot上传文件与物理删除
java·spring boot·后端
jay神6 小时前
基于SpringBoot的校园社团活动智能匹配与推荐系统
java·前端·spring boot·后端·毕业设计
Java程序员威哥6 小时前
Arthas+IDEA实战:Java线上问题排查完整流程(Spring Boot项目落地)
java·开发语言·spring boot·python·c#·intellij-idea
Elieal7 小时前
SpringBoot 中处理接口传参时常用的注解
java·spring boot·后端