news 2026/4/15 17:18:57

Springboot3 | JUnit 5 使用详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Springboot3 | JUnit 5 使用详解

Spring Boot 3 中 JUnit 5 使用详解

我们从「能用」到「用好」逐步拆解 Spring Boot 3 中 JUnit 5 的使用,全程结合实际开发场景,所有代码可直接运行。

基础认知:为什么要在 Spring Boot 中用 JUnit?

实际开发中,我们写的 Controller、Service、工具类都需要验证逻辑是否正确——比如用户注册时的参数校验、订单计算的金额是否准确。手动测试(比如启动项目调接口)效率低,而 JUnit 能让我们写「自动化测试用例」,代码写完就能验证,还能在打包、部署前自动执行,避免低级错误。

Spring Boot 3 内置了 JUnit 5(替代了老版本的 JUnit 4),核心依赖是spring-boot-starter-test,无需额外配置就能用。

第一步:环境准备

1. 创建 Spring Boot 3 项目

用 Spring Initializr 创建项目,选择:

  • Spring Boot 3.2+
  • 依赖:Spring WebSpring Boot Starter Test(自动包含 JUnit 5、AssertJ、Mockito 等)

2. 核心依赖(pom.xml 关键部分)

<dependencies><!-- Spring Boot 测试核心依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- Web 依赖(用于 Controller 测试) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency></dependencies>

第二步:入门案例——测试简单工具类(无 Spring 依赖)

先从「最基础的纯 Java 方法测试」入手,不依赖 Spring 容器,理解 JUnit 5 的核心注解。

场景:测试金额计算工具类

实际开发中,订单系统常需要计算折扣后金额,我们先写工具类,再写测试用例。

1. 待测试的工具类
// src/main/java/com/example/demo/util/PriceCalculator.javapackagecom.example.demo.util;/** * 金额计算工具类 */publicclassPriceCalculator{/** * 计算折扣后金额 * @param originalPrice 原价 * @param discountRate 折扣率(0.8 表示 8 折) * @return 折扣后金额(保留 2 位小数) */publicstaticdoublecalculateDiscountPrice(doubleoriginalPrice,doublediscountRate){// 边界校验:原价和折扣率不能为负if(originalPrice<0||discountRate<0){thrownewIllegalArgumentException("原价和折扣率不能为负数");}// 计算并保留 2 位小数doubleresult=originalPrice*discountRate;returnMath.round(result*100)/100.0;}}
2. JUnit 5 测试用例

测试类放在src/test/java下,包结构和主类一致:

// src/test/java/com/example/demo/util/PriceCalculatorTest.javapackagecom.example.demo.util;importorg.junit.jupiter.api.Test;importstaticorg.junit.jupiter.api.Assertions.*;/** * 金额计算工具类测试 */// JUnit 5 无需类级注解,直接写测试方法publicclassPriceCalculatorTest{// 测试正常场景:100 元打 8 折,预期 80.0@TestvoidtestCalculateDiscountPrice_Normal(){doubleresult=PriceCalculator.calculateDiscountPrice(100,0.8);// 断言:实际结果等于预期结果(允许 0.001 误差)assertEquals(80.0,result,0.001);}// 测试边界场景:原价为 0@TestvoidtestCalculateDiscountPrice_ZeroPrice(){doubleresult=PriceCalculator.calculateDiscountPrice(0,0.9);assertEquals(0.0,result);}// 测试异常场景:折扣率为负,预期抛出 IllegalArgumentException@TestvoidtestCalculateDiscountPrice_NegativeDiscount(){// 断言方法会抛出指定异常IllegalArgumentExceptionexception=assertThrows(IllegalArgumentException.class,()->PriceCalculator.calculateDiscountPrice(100,-0.5));// 验证异常信息assertEquals("原价和折扣率不能为负数",exception.getMessage());}}
运行测试
  • 在 IDEA 中,右键点击测试类 → RunPriceCalculatorTest
  • 控制台会显示测试结果:绿色对勾表示通过,红色叉号表示失败

核心知识点(入门级)

注解/方法作用
@Test标记测试方法,JUnit 会自动执行
assertEquals断言实际值等于预期值(支持数值、字符串、对象等)
assertThrows断言方法执行时会抛出指定类型的异常
assertTrue/assertFalse断言布尔值为 true/false

第三步:进阶案例——测试 Spring Bean(Service 层)

实际开发中,Service 层依赖 Repository、其他 Service,需要启动 Spring 容器才能测试。Spring Boot 提供了@SpringBootTest注解,自动加载上下文。

场景:测试用户服务(UserService)

用户服务包含「根据 ID 查询用户」「新增用户」逻辑,依赖模拟的 Repository。

1. 实体类
// src/main/java/com/example/demo/entity/User.javapackagecom.example.demo.entity;publicclassUser{privateLongid;privateStringname;privateIntegerage;// 构造器、getter/setter、toStringpublicUser(){}publicUser(Longid,Stringname,Integerage){this.id=id;this.name=name;this.age=age;}// getter/setter 省略(实际开发中用 Lombok 的 @Data 更方便)publicLonggetId(){returnid;}publicvoidsetId(Longid){this.id=id;}publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}publicIntegergetAge(){returnage;}publicvoidsetAge(Integerage){this.age=age;}@OverridepublicStringtoString(){return"User{"+"id="+id+", name='"+name+'\''+", age="+age+'}';}}
2. Repository 层(模拟)
// src/main/java/com/example/demo/repository/UserRepository.javapackagecom.example.demo.repository;importcom.example.demo.entity.User;importorg.springframework.stereotype.Repository;importjava.util.HashMap;importjava.util.Map;importjava.util.Optional;@RepositorypublicclassUserRepository{// 模拟数据库privatestaticfinalMap<Long,User>USER_DB=newHashMap<>();static{// 初始化测试数据USER_DB.put(1L,newUser(1L,"张三",20));USER_DB.put(2L,newUser(2L,"李四",25));}// 根据 ID 查询用户publicOptional<User>findById(Longid){returnOptional.ofNullable(USER_DB.get(id));}// 新增用户publicUsersave(Useruser){LongnewId=USER_DB.keySet().stream().max(Long::compare).orElse(0L)+1;user.setId(newId);USER_DB.put(newId,user);returnuser;}}
3. Service 层
// src/main/java/com/example/demo/service/UserService.javapackagecom.example.demo.service;importcom.example.demo.entity.User;importcom.example.demo.repository.UserRepository;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importjava.util.Optional;@ServicepublicclassUserService{@AutowiredprivateUserRepositoryuserRepository;/** * 根据 ID 查询用户 * @param id 用户 ID * @return 用户信息(若不存在则抛出异常) */publicUsergetUserById(Longid){returnuserRepository.findById(id).orElseThrow(()->newRuntimeException("用户不存在,ID:"+id));}/** * 新增用户(年龄校验:必须大于 0) * @param user 用户信息 * @return 新增后的用户(带 ID) */publicUsercreateUser(Useruser){if(user.getAge()==null||user.getAge()<=0){thrownewIllegalArgumentException("年龄必须大于 0");}returnuserRepository.save(user);}}
4. Service 层测试用例
// src/test/java/com/example/demo/service/UserServiceTest.javapackagecom.example.demo.service;importcom.example.demo.entity.User;importcom.example.demo.repository.UserRepository;importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importstaticorg.junit.jupiter.api.Assertions.*;/** * UserService 测试(启动 Spring 容器) */// 启动 Spring Boot 上下文,自动扫描 Bean@SpringBootTestpublicclassUserServiceTest{// 自动注入 Spring 容器中的 UserService@AutowiredprivateUserServiceuserService;// 自动注入 Repository(可选:用于验证数据)@AutowiredprivateUserRepositoryuserRepository;// 测试正常查询用户@TestvoidtestGetUserById_Success(){Useruser=userService.getUserById(1L);// 断言用户信息正确assertEquals("张三",user.getName());assertEquals(20,user.getAge());}// 测试查询不存在的用户(预期抛异常)@TestvoidtestGetUserById_NotFound(){RuntimeExceptionexception=assertThrows(RuntimeException.class,()->userService.getUserById(999L));assertEquals("用户不存在,ID:999",exception.getMessage());}// 测试新增用户(正常场景)@TestvoidtestCreateUser_Success(){// 准备测试数据UsernewUser=newUser();newUser.setName("王五");newUser.setAge(30);// 执行新增方法UsersavedUser=userService.createUser(newUser);// 断言结果assertNotNull(savedUser.getId());// ID 不为空assertEquals("王五",savedUser.getName());assertEquals(30,savedUser.getAge());// 验证 Repository 中确实存在该用户UserfoundUser=userRepository.findById(savedUser.getId()).orElse(null);assertNotNull(foundUser);}// 测试新增用户(年龄为负,预期抛异常)@TestvoidtestCreateUser_InvalidAge(){UserinvalidUser=newUser();invalidUser.setName("赵六");invalidUser.setAge(-5);IllegalArgumentExceptionexception=assertThrows(IllegalArgumentException.class,()->userService.createUser(invalidUser));assertEquals("年龄必须大于 0",exception.getMessage());}}

核心知识点(进阶级)

注解/特性作用
@SpringBootTest启动 Spring Boot 上下文,加载所有 Bean,模拟真实运行环境
@Autowired在测试类中注入 Spring 容器中的 Bean
assertNotNull断言对象不为 null(常用语验证返回的实体、ID 等)
测试隔离性每次测试方法执行后,Spring 上下文默认复用,但数据会重置(保证测试独立)

第四步:高级案例——测试 Controller 层(模拟 HTTP 请求)

实际开发中,Controller 层接收 HTTP 请求,返回响应,需要模拟接口调用。Spring Boot 提供了@WebMvcTest注解,专门测试 Controller,无需启动完整 Spring 上下文,效率更高。

场景:测试用户接口(UserController)

1. Controller 层
// src/main/java/com/example/demo/controller/UserController.javapackagecom.example.demo.controller;importcom.example.demo.entity.User;importcom.example.demo.service.UserService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.http.HttpStatus;importorg.springframework.http.ResponseEntity;importorg.springframework.web.bind.annotation.*;@RestController@RequestMapping("/api/users")publicclassUserController{@AutowiredprivateUserServiceuserService;/** * 根据 ID 查询用户 * @param id 用户 ID * @return 用户信息 */@GetMapping("/{id}")publicResponseEntity<User>getUserById(@PathVariableLongid){Useruser=userService.getUserById(id);returnResponseEntity.ok(user);}/** * 新增用户 * @param user 用户信息 * @return 新增后的用户 */@PostMappingpublicResponseEntity<User>createUser(@RequestBodyUseruser){UsersavedUser=userService.createUser(user);returnResponseEntity.status(HttpStatus.CREATED).body(savedUser);}}
2. Controller 层测试用例
// src/test/java/com/example/demo/controller/UserControllerTest.javapackagecom.example.demo.controller;importcom.example.demo.entity.User;importcom.example.demo.service.UserService;importcom.fasterxml.jackson.databind.ObjectMapper;importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;importorg.springframework.boot.test.mock.mockito.MockBean;importorg.springframework.http.MediaType;importorg.springframework.test.web.servlet.MockMvc;importstaticorg.mockito.ArgumentMatchers.any;importstaticorg.mockito.Mockito.when;importstaticorg.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;importstaticorg.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;importstaticorg.springframework.test.web.servlet.result.MockMvcResultMatchers.*;/** * UserController 测试(仅启动 Web 层,模拟 HTTP 请求) */// 仅加载 Web 相关 Bean(Controller、HandlerMapping 等),不加载 Service/Repository@WebMvcTest(UserController.class)publicclassUserControllerTest{// 模拟 HTTP 请求的核心工具@AutowiredprivateMockMvcmockMvc;// 序列化/反序列化 JSON(用于请求体转换)@AutowiredprivateObjectMapperobjectMapper;// 模拟 UserService(避免依赖真实 Service,解耦测试)@MockBeanprivateUserServiceuserService;// 测试查询用户接口(成功场景)@TestvoidtestGetUserById_Success()throwsException{// 1. 模拟 Service 返回数据UsermockUser=newUser(1L,"张三",20);when(userService.getUserById(1L)).thenReturn(mockUser);// 2. 模拟 GET 请求,并验证响应mockMvc.perform(get("/api/users/1")// 请求路径.contentType(MediaType.APPLICATION_JSON))// 请求类型.andExpect(status().isOk())// 响应状态码 200.andExpect(jsonPath("$.id").value(1))// 响应 JSON 的 id 字段为 1.andExpect(jsonPath("$.name").value("张三"))// name 字段为 张三.andExpect(jsonPath("$.age").value(20));// age 字段为 20}// 测试查询用户接口(失败场景)@TestvoidtestGetUserById_NotFound()throwsException{// 1. 模拟 Service 抛异常when(userService.getUserById(999L)).thenThrow(newRuntimeException("用户不存在,ID:999"));// 2. 模拟 GET 请求,验证响应mockMvc.perform(get("/api/users/999").contentType(MediaType.APPLICATION_JSON)).andExpect(status().is5xxServerError())// 响应状态码 500.andExpect(content().string(containsString("用户不存在,ID:999")));// 响应内容包含异常信息}// 测试新增用户接口(成功场景)@TestvoidtestCreateUser_Success()throwsException{// 1. 准备测试数据UserrequestUser=newUser();requestUser.setName("王五");requestUser.setAge(30);UserresponseUser=newUser(3L,"王五",30);// 2. 模拟 Service 返回数据when(userService.createUser(any(User.class))).thenReturn(responseUser);// 3. 模拟 POST 请求,验证响应mockMvc.perform(post("/api/users").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(requestUser)))// 请求体转 JSON.andExpect(status().isCreated())// 响应状态码 201.andExpect(jsonPath("$.id").value(3)).andExpect(jsonPath("$.name").value("王五"));}}

核心知识点(高级)

注解/工具作用
@WebMvcTest仅加载 Web 层 Bean,专注测试 Controller,启动速度比@SpringBootTest
MockMvc模拟 HTTP 请求(GET/POST/PUT/DELETE),无需启动服务器
@MockBean模拟 Service/Repository,解耦测试(不依赖真实实现)
jsonPath解析响应 JSON,验证字段值(如$.name表示 JSON 中的 name 字段)
ObjectMapper将 Java 对象转为 JSON 字符串(用于构造请求体)

第五步:实战技巧(贴近真实开发)

1. 测试命名规范

测试方法名要清晰,一眼看出「测试场景 + 预期结果」,比如:

  • testGetUserById_Success(查询用户-成功)
  • testCreateUser_InvalidAge(新增用户-年龄无效)

2. 测试分层策略

层级测试注解核心目标
工具类无(纯 JUnit)验证逻辑正确性
Service@SpringBootTest验证业务逻辑、依赖调用
Controller@WebMvcTest验证请求映射、参数解析、响应

3. 跳过测试

个别测试暂时不想运行,用@Disabled注解:

@Test@Disabled("暂时跳过,待修复 XXX 问题")voidtestTempSkip(){// ...}

4. 测试生命周期

注解作用
@BeforeEach每个测试方法执行前执行(比如初始化测试数据)
@AfterEach每个测试方法执行后执行(比如清理数据)
@BeforeAll所有测试方法执行前执行一次(静态方法)
@AfterAll所有测试方法执行后执行一次(静态方法)

示例:

@BeforeEachvoidsetUp(){// 每个测试方法执行前初始化数据System.out.println("开始执行测试方法...");}

总结

Spring Boot 3 中 JUnit 5 的使用遵循「由浅入深」的逻辑:

  1. 纯 Java 方法:直接用 JUnit 核心断言,无需 Spring;
  2. Spring Bean:用@SpringBootTest启动容器,注入 Bean 测试;
  3. Web 层:用@WebMvcTest+MockMvc模拟 HTTP 请求,解耦测试。

实际开发中,写测试用例不是「额外工作」,而是「提效手段」——能提前发现 bug,减少手动测试成本,尤其是在迭代升级时,修改代码后跑一遍测试,就能快速验证是否影响原有功能。

所有代码均可直接复制到 Spring Boot 3 项目中运行,建议先跑通基础案例,再逐步尝试 Service 和 Controller 层测试,加深理解。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/14 21:55:23

5分钟快速上手:Farfalle Serper搜索API终极替代方案

5分钟快速上手&#xff1a;Farfalle Serper搜索API终极替代方案 【免费下载链接】farfalle &#x1f50d; ai search engine - run local or cloud language models 项目地址: https://gitcode.com/GitHub_Trending/fa/farfalle 还在为Google搜索API的复杂配置和高昂费用…

作者头像 李华
网站建设 2026/4/15 13:47:54

Wan2.2-T2V-A14B支持720P输出的背后:对显存和算力的真实需求分析

Wan2.2-T2V-A14B支持720P输出的背后&#xff1a;对显存和算力的真实需求分析 在AI生成内容&#xff08;AIGC&#xff09;的浪潮中&#xff0c;视频生成正成为下一个爆发点。如果说文本生成和图像生成已经让大众感受到“智能创作”的威力&#xff0c;那么文本到视频&#xff08;…

作者头像 李华
网站建设 2026/4/15 13:48:55

怎么用低成本打造一个高效精准的制造业客户获取系统呢?

业获客系统的必要性与优势在制造业中&#xff0c;建立一个获客系统不仅能提升竞争力&#xff0c;同时也是企业持续发展的有力保障。我们都知道&#xff0c;现在市场变化极快&#xff0c;传统的获客方式已经不能满足企业不断增长的需求。因此&#xff0c;采用智能获客系统就显得…

作者头像 李华
网站建设 2026/4/15 7:13:53

11、树莓派远程控制机器人开发全攻略

树莓派远程控制机器人开发全攻略 在科技飞速发展的今天,利用树莓派开发远程控制机器人成为了许多爱好者和开发者热衷的项目。本文将详细介绍如何使用树莓派开发一个远程控制机器人,并实现实时视频流和实时距离测量功能。 1. 准备工作 在开始开发之前,我们需要准备以下组件…

作者头像 李华
网站建设 2026/4/2 1:35:30

DeepWiki-Open智能文档生成器:彻底改变你的代码文档工作流程

DeepWiki-Open智能文档生成器&#xff1a;彻底改变你的代码文档工作流程 【免费下载链接】deepwiki-open Open Source DeepWiki: AI-Powered Wiki Generator for GitHub Repositories 项目地址: https://gitcode.com/gh_mirrors/de/deepwiki-open 还在为项目文档发愁吗&…

作者头像 李华
网站建设 2026/4/14 15:00:04

Tabler Icons图标库:5分钟从入门到精通

Tabler Icons图标库&#xff1a;5分钟从入门到精通 【免费下载链接】tabler-icons A set of over 4800 free MIT-licensed high-quality SVG icons for you to use in your web projects. 项目地址: https://gitcode.com/gh_mirrors/ta/tabler-icons 还在为项目图标不够…

作者头像 李华