当前位置: 首页 > article >正文

Spring Boot框架下的单元测试

1. 什么是单元测试

1.1 基本定义

  • 单元测试(Unit Test) 是对软件开发中最小可测单位(例如一个方法或者一个类)进行验证的一种测试方式。
  • 在 Java 后端的 Spring Boot 项目中,单元测试通常会借助 JUnitMockito 等框架对代码中核心逻辑进行快速且隔离的验证,保证功能正确性。

目的:及早发现并修复 BUG,使后续迭代功能或重构时能迅速验证不会破坏已实现的功能。

1.2 单元测试在 Spring Boot 中的地位

  • Spring Boot 提供了非常方便的测试支持,如 @SpringBootTest@TestConfiguration 等注解,让开发者可以快速地在带有 Spring 容器上下文的环境中执行测试。
  • Spring Boot 本身也对 JUnit、Mockito、AssertJ 等常用测试框架或库提供了开箱即用的整合或依赖。

1.3 单元测试与其他测试的区别

  • 单元测试:聚焦在一个方法或者一个类层面,不涉及过多外部依赖,能极快地发现逻辑错误。
  • 集成测试:多个模块或组件交互时的测试,通常依赖真实数据库、消息队列等外部资源。
  • 端到端测试(E2E):关注的是整个系统的完整流程,包括前端、后端、数据库、外部接口等。
  • 在 Spring Boot 环境中,可以使用 @SpringBootTest 搭配 Mock 或者内存数据库来实现集成测试,但这通常已经不只是“单元”级别了。

2. 为什么要写单元测试?

  • 快速发现 Bug:写完代码马上测,不用等到上线才被发现问题。
  • 减少回归成本:以后代码改动或升级,只要一键跑测试,就能知道改动有没有影响其他功能。
  • 保证代码质量:养成单元测试的习惯,会促使你把代码设计得更简洁和更容易测试。

简单说:花小时间写单元测试,能为你省下大时间修 Bug。


3. 环境准备

3.1 依赖

在一个常规的 Spring Boot 项目中,只要在 pom.xml(Maven)或 build.gradle(Gradle) 里加上:

<!-- 如果是 Maven -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
  • JUnit 5:最常用的Java测试框架(写 @Test 方法)
  • Mockito:常用的“模拟”库(用来Mock其他依赖)
  • AssertJ / Hamcrest:更好用的断言库
  • Spring Test / Spring Boot Test:Spring官方提供的测试辅助

这也就够了,一般不需要额外安装别的。

3.2 项目结构

Spring Boot常见的目录结构(Maven示例):

src
 ├─ main
 │   └─ java
 │       └─ com.example.demo
 │           ├─ DemoApplication.java
 │           └─ service
 │               └─ MyService.java
 └─ test
     └─ java
         └─ com.example.demo
             ├─ DemoApplicationTests.java
             └─ service
                 └─ MyServiceTest.java
  • src/main/java 放你的业务代码
  • src/test/java 放你的测试代码
  • 通常测试类的包路径要和被测类一致,这样在IDE里能很快对上号,也方便管理。

4. 最最简单的单元测试示例(不依赖Spring)

先从“纯JUnit”说起,最简单的情况就是:

  • 我有一个普通的工具类/方法
  • 我就想测试它的输入输出对不对
  • 不用装载Spring,也不用什么复杂注解

代码示例

假设我们有一个简单的工具类:

public class MathUtil {
    public static int add(int a, int b) {
        return a + b;
    }
}

那我们写一个测试类(路径:src/test/java/.../MathUtilTest.java):

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class MathUtilTest {

    @Test
    void testAdd() {
        int result = MathUtil.add(2, 3);
        Assertions.assertEquals(5, result, "2 + 3 应该等于 5");
    }
}
  • @Test 表示这是一个测试方法。
  • Assertions.assertEquals(期望值, 实际值, "提示信息") 用来断言。
    • 如果断言不通过,测试就失败;通过则测试成功。

运行方法:

  • 在 IDE(如 IntelliJ/ Eclipse)里,右键这个 MathUtilTest 类 -> Run 'MathUtilTest'
  • 或者在命令行里运行 mvn test(Maven) / gradle test(Gradle)。

这就是最最基础的单元测试


5. 在 Spring Boot 里测试 - Service层

当你要测试一个 Service(业务逻辑类) 时,它可能依赖其他Bean(例如 Repository、Dao 等)或者需要 Autowired。在 Spring Boot 里,有两种主要方法:

方法1:纯Mock(不启动Spring Context)

适合只想测试这个Service逻辑本身,不需要真的连数据库,也不需要整个Spring环境。速度最快。

  • 用 Mockito 来创建一个假的(Mock)依赖。
  • 注入到要测的Service里,这样你可以控制依赖的行为。

示例

UserRepository.java (假设它是个接口,用来访问数据库):

public interface UserRepository {
    User findByName(String name);
    // ... 其他方法
}

UserService.java (我们要测这个类):

public class UserService {

    private UserRepository userRepository;

    // 通过构造注入依赖
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public String getUserNickname(String name) {
        User user = userRepository.findByName(name);
        if (user == null) {
            return "UNKNOWN";
        }
        return user.getNickname();
    }
}

UserServiceTest.java (测试类,不依赖 Spring):

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Assertions;
import org.mockito.Mockito;
import org.mockito.Mock;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(MockitoExtension.class) // JUnit5 启用Mockito
public class UserServiceTest {

    @Mock
    private UserRepository userRepository; // Mock出来的依赖

    @InjectMocks
    private UserService userService;       // 要测试的对象,会把上面这个Mock自动注入进来

    @Test
    void testGetUserNickname_found() {
        // 1. 假设我们模拟一个“数据库中查到的用户”:
        User mockUser = new User();
        mockUser.setName("alice");
        mockUser.setNickname("AliceWonder");

        // 2. 定义假数据的返回行为
        Mockito.when(userRepository.findByName("alice")).thenReturn(mockUser);

        // 3. 调用被测方法
        String nickname = userService.getUserNickname("alice");

        // 4. 断言结果
        Assertions.assertEquals("AliceWonder", nickname);
    }

    @Test
    void testGetUserNickname_notFound() {
        // 没有设置when,则默认返回null
        String nickname = userService.getUserNickname("bob");
        Assertions.assertEquals("UNKNOWN", nickname);
    }
}
  • 使用了 @Mock 注解声明要模拟的依赖 userRepository
  • 使用了 @InjectMocks 注解告诉 Mockito,要把所有标记 @Mock 的对象注入进 UserService
  • 这样就能让 UserService 这个对象在执行时使用模拟过的 userRepository 而不访问真实数据库。
  • 然后通过 Mockito.when(...) 来定义依赖方法的返回值,用于测试用例的前提条件设置。
  • 通过 Assertions 来验证执行结果是否符合预期。

这样就只测 UserService 的逻辑,不会真的访问数据库,也不需要启动Spring,执行很快。

方法2:使用 @SpringBootTest (集成上下文)

适合你想在测试时使用Spring管理Bean,比如自动注入 @Autowired,或想测试和别的Bean的连接配置是否正常。

  • 在测试类上加 @SpringBootTest
  • 这样Spring容器会启动,你也能 @Autowired 你的Service或者别的Bean。

示例

UserService.java (类似前面,只不过换成了 Spring注入):

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public String getUserNickname(String name) {
        User user = userRepository.findByName(name);
        if (user == null) {
            return "UNKNOWN";
        }
        return user.getNickname();
    }
}

UserServiceSpringTest.java (测试类,使用Spring上下文):

@SpringBootTest
public class UserServiceSpringTest {

    @Autowired
    private UserService userService;

    @MockBean
    private UserRepository userRepository; 
    // @MockBean的意思:Spring 启动时,
    // 把真正的UserRepository替换成一个Mock对象,
    // 我们就可以定义它的返回值,而不会真的连数据库

    @Test
    void testGetUserNickname_found() {
        User mockUser = new User();
        mockUser.setName("alice");
        mockUser.setNickname("AliceWonder");

        Mockito.when(userRepository.findByName("alice")).thenReturn(mockUser);

        String result = userService.getUserNickname("alice");
        Assertions.assertEquals("AliceWonder", result);
    }

    @Test
    void testGetUserNickname_notFound() {
        // 不设置when就会返回null
        String result = userService.getUserNickname("unknown");
        Assertions.assertEquals("UNKNOWN", result);
    }
}
  • @SpringBootTest会启动一个小型Spring环境,让 @Autowired 能起作用。
  • @MockBean 可以让你把某个Bean(比如 UserRepository)变成一个模拟对象。
  • 整体执行依然比较快,但比纯Mock稍微慢一点,因为要先启动Spring容器。

6. 测试 Controller 层

在 Spring Boot 里,Controller 是对外的 HTTP 接口。最常见的两种测试方式:

  • @WebMvcTest + MockMvc:不启动整个应用,只启动Web层,速度较快;
  • @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + TestRestTemplate:会真正启动一个内嵌服务器,发起真实HTTP请求,更贴近实际环境。

6.1 @WebMvcTest 示例

@WebMvcTest(UserController.class) // 表示只测 UserController 相关
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc; // 用来模拟HTTP请求

    @MockBean
    private UserService userService; // Mock掉Service层

    @Test
    void testGetUser() throws Exception {
        // 假设Service返回一个User对象
        User mockUser = new User();
        mockUser.setName("test");
        mockUser.setNickname("TestNick");

        // 定义service行为
        Mockito.when(userService.getUserNickname("test")).thenReturn("TestNick");

        // 用MockMvc发起GET请求,对应Controller的 /user/{name} 路径
        mockMvc.perform(MockMvcRequestBuilders.get("/user/test"))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.content().string("TestNick"));
    }
}
  • @WebMvcTest 只会扫描和加载 Web 层相关的组件,不会启动整个 Spring Boot 应用,测试速度更快。
  • mockMvc.perform(get("/users/1")) 可以模拟一次 GET 请求到 /users/1,并断言返回的 JSON 结构和内容。

6.2 @SpringBootTest + TestRestTemplate

如果你想做一个更真实的集成测试(包括 Controller、Service、Repository 等所有层),可以使用 @SpringBootTest 并设置 webEnvironment = RANDOM_PORTDEFINED_PORT 来启动内嵌服务器,然后注入 TestRestTemplate 来请求: 

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate; // 可以真的发请求

    @Test
    void testGetUser() {
        // 假设数据库里已经有对应数据,或者你用 @MockBean 替换依赖
        String result = restTemplate.getForObject("/user/test", String.class);
        Assertions.assertEquals("TestNick", result);
    }
}
  • 这里会真正启动一个随机端口的Tomcat,然后 TestRestTemplate 真的去请求本地这个 /user/test 接口。
  • 非常贴近真实部署,只是适合做集成测试,比前面的MockMvc测试稍慢一点。

7. 常见的断言与技巧

7.1 断言

  • Assertions.assertEquals(期望, 实际):断言二者相等。
  • Assertions.assertTrue(条件):断言条件为真。
  • Assertions.assertThrows(异常类型, 代码块):断言执行代码块会抛出指定异常。

例如:

@Test
void testThrowException() {
    Assertions.assertThrows(IllegalArgumentException.class, () -> {
        // 假设调用了一个会抛出异常的方法
        someMethod(null);
    });
}

7.2 Mock时常用的 Mockito 方法

  • Mockito.when( mockObj.方法(...) ).thenReturn(返回值);
  • Mockito.when( mockObj.方法(...) ).thenThrow(异常);
  • Mockito.verify( mockObj, Mockito.times(1) ).某方法(...); // 验证是否调用了某方法

8. 测试运行与整合

8.1 在本地IDE里运行

  • 右键单个测试类或测试方法 -> Run
  • 或者在项目主目录运行 mvn test / gradle test

8.2 与持续集成(CI)整合

  • 在 Jenkins、GitLab CI、GitHub Actions 等环境里,一般只要执行 mvn testgradle test 就可以跑所有测试用例。
  • 如果测试全部通过,就说明代码基本没问题;如果测试挂了,说明你这次提交的改动有Bug或者破坏了原有逻辑。

9. 流程小结(简版“使用指南”)

  • 新手首次写单元测试

    • src/test/java 下创建和源代码同包路径的测试类:XXXTest.java
    • 在类里加 @Test 注解的方法,里面写 Assertions.assertXXX(...)
    • 右键运行,看输出是否通过。
  • 要测Service逻辑,但不想连数据库

    • 在测试类上写:
      @ExtendWith(MockitoExtension.class)
      public class MyServiceTest {
          @Mock
          private MyRepository myRepository;
      
          @InjectMocks
          private MyService myService;
          ...
      }
      
    • Mockito.when(...) 来模拟依赖。
    • assertEquals(...) 来判断结果。
  • 要测Service逻辑,并用Spring上下文

    • 在测试类上加 @SpringBootTest
    • 注入 Service:@Autowired private MyService myService;
    • 如果你不想真的连数据库,那就用 @MockBean MyRepository myRepository;
  • 要测Controller

    • @WebMvcTest(MyController.class) + @MockBean MyService myService; + MockMvc 做单元测试,速度较快;
    • 或者用 @SpringBootTest(webEnvironment = ... ) + TestRestTemplate 做近似真实的集成测试。

10. 其他常见问题

  • 测试和生产环境的配置冲突了怎么办?
    • 可以在 application-test.yml 里放测试专用配置,然后在测试时用 spring.profiles.active=test
  • 需要数据库的测试怎么办?
    • 可以用@DataJpaTest+内存数据库(比如 H2),只测JPA相关逻辑,不影响真数据库。
  • 想看覆盖率怎么办?
    • 可以集成 Jacoco 插件,跑 mvn test 后生成覆盖率报告,看你的测试是不是覆盖到了主要逻辑。
  • 测试很慢怎么办?
    • 如果你的逻辑不是必须要Spring,就尽量用纯Mock,不用 @SpringBootTest
    • 如果只是测Controller,就用 @WebMvcTest,不要启动全部。

http://www.kler.cn/a/531728.html

相关文章:

  • 重新刷题求职2-DAY1
  • Rust中使用ORM框架diesel报错问题
  • 网络原理(4)—— 网络层详解
  • 【Linux系统】信号:信号保存 / 信号处理、内核态 / 用户态、操作系统运行原理(中断)
  • Elasticsearch的索引生命周期管理
  • LabVIEW如何高频采集温度数据?
  • 3 Yarn
  • JAVA实战开源项目:学科竞赛管理系统(Vue+SpringBoot) 附源码
  • 我的AI工具箱Tauri版-ZoomImageSDXL全图超清放大TILE+SDXL
  • DOM 操作入门:HTML 元素操作与页面事件处理
  • JVM执行流程与架构(对应不同版本JDK)
  • 记忆化搜索和动态规划 --最长回文子串为例
  • EtherCAT主站IGH-- 29 -- IGH之mailbox.h/c文件解析
  • Skyeye 云 VUE 版本 v3.15.7 发布
  • 996引擎-怪物:添加怪物
  • 对象的实例化、内存布局与访问定位
  • 大型语言模型(LLMs)研究综述:进展、挑战与展望 ——《A Survey of Large Language Models》
  • Jupyterlab和notebook修改文件的默认存放路径的方法
  • python-文件操作笔记
  • 少样本提示词模板
  • C语言教学第三课:运算符与表达式
  • 【25考研】南开大学计算机复试攻略及注意事项
  • 五子棋对弈
  • DeepSeek文生图模型Janus-Pro论文解读 —— 多模态AI的革命?
  • 如何解决云台重力补偿?
  • 传输层协议 UDP 与 TCP