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

相关推荐
qq_12498707531 小时前
基于SSM的动物保护系统的设计与实现(源码+论文+部署+安装)
java·数据库·spring boot·毕业设计·ssm·计算机毕业设计
Coder_Boy_1 小时前
基于SpringAI的在线考试系统-考试系统开发流程案例
java·数据库·人工智能·spring boot·后端
2301_818732061 小时前
前端调用控制层接口,进不去,报错415,类型不匹配
java·spring boot·spring·tomcat·intellij-idea
汤姆yu5 小时前
基于springboot的尿毒症健康管理系统
java·spring boot·后端
暮色妖娆丶5 小时前
Spring 源码分析 单例 Bean 的创建过程
spring boot·后端·spring
biyezuopinvip6 小时前
基于Spring Boot的企业网盘的设计与实现(任务书)
java·spring boot·后端·vue·ssm·任务书·企业网盘的设计与实现
JavaGuide6 小时前
一款悄然崛起的国产规则引擎,让业务编排效率提升 10 倍!
java·spring boot
figo10tf7 小时前
Spring Boot项目集成Redisson 原始依赖与 Spring Boot Starter 的流程
java·spring boot·后端
zhangyi_viva7 小时前
Spring Boot(七):Swagger 接口文档
java·spring boot·后端
橙露7 小时前
Spring Boot 核心原理:自动配置机制与自定义 Starter 开发
java·数据库·spring boot