news 2026/2/17 10:02:16

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/2/15 1:06:40

Lottie动画实战手册:从AE设计到Web渲染的避坑指南

Lottie动画实战手册&#xff1a;从AE设计到Web渲染的避坑指南 【免费下载链接】lottie-web 项目地址: https://gitcode.com/gh_mirrors/lot/lottie-web 是不是经常遇到这样的场景&#xff1f;设计师在After Effects里精心制作的动画&#xff0c;到了开发这边就变成了&q…

作者头像 李华
网站建设 2026/2/17 4:55:54

微博超话自动签到工具:3分钟掌握自动化管理终极懒人指南

微博超话自动签到工具&#xff1a;3分钟掌握自动化管理终极懒人指南 【免费下载链接】weibo_supertopic_sign 基于Python/Nodejs的微博超话签到脚本&#xff0c;支持云函数运行或青龙面板运行 项目地址: https://gitcode.com/gh_mirrors/we/weibo_supertopic_sign 还在为…

作者头像 李华
网站建设 2026/2/17 7:01:43

大规模语言模型的元认知能力评估与增强

大规模语言模型的元认知能力评估与增强关键词&#xff1a;大规模语言模型、元认知能力、评估、增强、自然语言处理摘要&#xff1a;本文聚焦于大规模语言模型的元认知能力&#xff0c;旨在深入探讨其评估与增强的相关问题。首先介绍了研究背景&#xff0c;明确目的、范围、预期…

作者头像 李华
网站建设 2026/2/9 6:59:49

昇腾NPU性能调优实战:从延迟优化到端侧部署的完整指南

昇腾NPU性能调优实战&#xff1a;从延迟优化到端侧部署的完整指南 【免费下载链接】openPangu-Embedded-1B-V1.1 昇腾原生的开源盘古 Embedded-1B-V1.1 语言模型 项目地址: https://ai.gitcode.com/ascend-tribe/openPangu-Embedded-1B-V1.1 还在为昇腾NPU上AI推理性能不…

作者头像 李华
网站建设 2026/2/11 8:36:51

5分钟掌握:开源AI图像编辑工具的极致效率方案

在当今AI图像编辑领域&#xff0c;开源工具正以前所未有的速度重新定义创作效率。Qwen-Image-Edit-Rapid-AIO作为本地部署方案的代表&#xff0c;将专业级图像编辑门槛降至"4步操作8秒生成"&#xff0c;为中小企业和独立创作者提供了高性价比的技术解决方案。 【免费…

作者头像 李华
网站建设 2026/2/6 14:34:07

Gitfiti深度解析:如何在GitHub贡献日历上绘制像素艺术?

Gitfiti深度解析&#xff1a;如何在GitHub贡献日历上绘制像素艺术&#xff1f; 【免费下载链接】gitfiti abusing github commit history for the lulz 项目地址: https://gitcode.com/gh_mirrors/gi/gitfiti 你是否曾见过GitHub个人主页上那些奇妙的像素图案&#xff0…

作者头像 李华