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

SpringBoot整合JUnit:单元测试从入门到精通

开篇:为什么要学 JUnit?先解决一个痛点
你有没有过这样的经历:
写了一个UserService,里面有个getUserById方法,为了测试它,得手动写个main方法,new 对象、调方法、打印结果;改一行代码后,又要重新跑main—— 既麻烦又容易漏测。

在软件开发中,单元测试是保证代码质量的重要手段。它能够:
及早发现Bug:在开发阶段发现问题,降低修复成本
确保代码质量:验证每个单元功能的正确性
支持重构:提供安全网,让重构更有信心
文档作用:测试用例本身就是最好的使用示例
SpringBoot 还专门整合了 JUnit,不用自己搭环境,开箱即用。

一、先搞懂:JUnit 和 SpringBoot Test 是什么关系?

  • JUnitJava 官方的单元测试框架,负责 “执行测试方法、判断结果是否正确”(比如断言assertEquals(5, user.getAge()));
  • SpringBoot TestSpringBoot 提供的测试 Starter(spring-boot-starter-test),已经整合了 JUnit、SpringTest 等工具,能自动启动 Spring 容器,帮你注入要测试的 Bean(比如UserService),不用手动 new 对象。
    简单说:spring-boot-starter-test = JUnit + Spring 容器支持,小白直接用这个 Starter 就行。

二、整合步骤:3 步搞定

以 “测试UserService的getUserById方法” 为例,手把手教你做。

步骤 1:导入依赖(pom.xml)
SpringBoot 项目创建时,默认会带这个依赖;如果没有,手动加:

<!-- SpringBoot测试 Starter(包含JUnit、SpringTest等) -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope> <!-- 仅测试环境生效,不影响生产 -->
</dependency>

说明scope=test表示这个依赖只在src/test/java目录下可用,不会打包到生产环境,很安全。

步骤 2:编写测试类(核心)
src/test/java目录下,创建和主程序对应的包(比如主程序包是com.heima,测试包也叫com.heima),然后写测试类。
核心注解:@SpringBootTest
这个注解是 “灵魂”,作用是:

  • 启动 SpringBoot 容器(和你运行主程序时一样);
  • 自动扫描 Bean,支持@Autowired注入要测试的对象。

两种写法(根据测试类位置选)
写法 1:测试类在 “启动类的包或子包下”(推荐)
如果你的 SpringBoot 启动类是com.heima.Springboot05JUnitApplication,测试类放在com.heimacom.heima.service等子包下,可以省略classes属性(Spring 会自动找启动类):

// 测试类包:com.heima.service(启动类包com.heima的子包)
import com.heima.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;// 不用写classes,自动找启动类
@SpringBootTest
class UserServiceTest {// 自动注入要测试的UserService(Spring容器里的Bean)@Autowiredprivate UserService userService;// 测试方法:用@Test注解标记@Testvoid testGetUserById() {// 1. 调用要测试的方法User user = userService.getUserById(1L);// 2. 断言结果是否正确(JUnit的核心功能)// 比如断言用户姓名是“张三”,id是1org.junit.jupiter.api.Assertions.assertEquals("张三", user.getName());org.junit.jupiter.api.Assertions.assertEquals(1L, user.getId());}
}

写法 2:测试类不在 “启动类的包下”(需指定启动类)
如果测试类放在别的包(比如com.test),必须用classes指定启动类的全路径,否则 Spring 找不到容器配置:

// 测试类包:com.test(和启动类包com.heima无关)
import com.heima.Springboot05JUnitApplication;
import com.heima.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;// 必须指定启动类,否则报错
@SpringBootTest(classes = Springboot05JUnitApplication.class)
class UserServiceTest {@Autowiredprivate UserService userService;@Testvoid testGetUserById() {// 测试逻辑同上...}
}

步骤 3:运行测试,查看结果
在测试方法(testGetUserById)左侧,点击 “运行按钮”(IDEA 里是绿色三角),等待运行完成:

  • 成功:控制台无报错,断言通过,方法名旁显示绿色对勾;
  • 失败:控制台会显示具体原因(比如断言失败:预期 “张三”,实际 “李四”;或 Bean 注入失败)。

三、常用测试注解

除了@SpringBootTest和@Test,还有几个常用注解,能应对复杂测试场景(比如测试前初始化数据、测试后清理数据):

注解作用示例场景
@Test标记这是一个测试方法,JUnit 会执行它每个要测试的接口 / 方法对应一个@Test方法
@BeforeEach每个@Test方法执行前都会运行测试前初始化数据库数据、创建临时文件
@AfterEach每个@Test方法执行后都会运行测试后删除临时数据、关闭连接
@BeforeAll所有@Test方法执行前运行一次(静态方法)初始化全局资源(比如启动 Redis)
@AfterAll所有@Test方法执行后运行一次(静态方法)释放全局资源(比如关闭 Redis 连接)

配置测试环境

// 配置随机端口用于Web测试
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class WebEnvironmentTest {// 测试代码
}// 配置Mock Web环境
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class MockWebTest {// 测试代码
}

示例:用 @BeforeEach 初始化数据

@SpringBootTest
class UserServiceTest {@Autowiredprivate UserService userService;// 测试前插入测试数据@BeforeEachvoid setUp() {// 比如往数据库插一条测试用户(id=1,name=张三)User testUser = new User();testUser.setId(1L);testUser.setName("张三");userService.save(testUser);}// 测试后删除测试数据@AfterEachvoid tearDown() {userService.deleteById(1L);}@Testvoid testGetUserById() {User user = userService.getUserById(1L);Assertions.assertEquals("张三", user.getName());}
}

测试方法注解

@SpringBootTest
class AnnotationTest {@Testvoid standardTest() {// 标准测试方法}@Disabled("暂时禁用,待修复")@Testvoid disabledTest() {// 被禁用的测试}@RepeatedTest(3)void repeatedTest() {// 重复执行3次的测试}
}

测试配置与属性覆盖

@SpringBootTest
// 覆盖应用配置,使用测试数据库等
@TestPropertySource(properties = {"spring.datasource.url=jdbc:h2:mem:testdb","spring.jpa.hibernate.ddl-auto=create-drop"
})
class WithCustomPropertiesTest {@Testvoid testWithCustomConfig() {// 使用自定义配置进行测试}
}

测试事务管理

@SpringBootTest
@Transactional // 测试方法在事务中运行,测试完成后自动回滚
class TransactionalTest {@Testvoid testWithTransaction() {// 数据库操作会在测试后回滚,不会影响数据库状态}
}

测试超时设置

@Test
@Timeout(value = 5, unit = TimeUnit.SECONDS) // 设置测试超时时间
void testWithTimeout() {// 如果执行超过5秒,测试将失败
}

四、测试最佳实践

1. 测试命名规范:方法名应该清晰表达测试意图

// 好的命名
@Test
void shouldReturnUserWhenValidIdProvided() {}// 不好的命名
@Test
void test1() {}

2. 遵循AAA模式:Arrange-Act-Assert(准备-执行-断言)

@Test
void testUserCreation() {// Arrange: 准备测试数据User user = new User("testuser", "test@example.com");// Act: 执行测试操作User result = userService.createUser(user);// Assert: 验证结果assertNotNull(result.getId());
}

3. 保持测试独立:每个测试应该独立运行,不依赖其他测试的状态

4. 测试异常情况:不仅要测试正常流程,还要测试异常情况

@Test
void shouldThrowExceptionWhenUserNotFound() {assertThrows(UserNotFoundException.class, () -> {userService.getUserById(999L);});
}

五、避坑指南:3 个常见问题及解决

1. 测试类注入 Bean 失败(报 NullPointerException)?
原因 1:测试类没加@SpringBootTest—— 必须加,否则 Spring 不启动容器,@Autowired无效;
原因 2:测试类不在启动类包下,且没指定classes—— 按 “写法 2” 补全@SpringBootTest(classes=xxx.class);
原因 3:要测试的类没加@Service/@Component等注解 —— 确保被 Spring 扫描为 Bean。

2. @Test 注解标红,找不到?
原因:导错包了!JUnit 5 的@Test是org.junit.jupiter.api.Test,不是 JUnit 4 的org.junit.Test;
解决:删除错误导入,重新导入import org.junit.jupiter.api.Test;(SpringBoot 2.2 + 默认用 JUnit 5)。

3. 运行测试时,启动类找不到?
原因:测试类的包路径和启动类差距太大(比如启动类在com.heima,测试类在com.abc.def);
解决:要么把测试类移到com.heima包下,要么在@SpringBootTest里指定classes。

总结:核心要点一句话记

  1. 依赖:加spring-boot-starter-test,不用自己配 JUnit;
  2. 注解:测试类加@SpringBootTest(包不对就加classes),测试方法加@Test;
  3. 注入:用@Autowired拿要测试的 Bean,不用手动 new;
  4. 断言:用Assertions判断结果是否正确,这是测试的核心。

JUnit 的本质是 “用代码测试代码”,写测试类虽然多花 5 分钟,但改代码后能一键验证,避免线上出问题 —— 这才是 “高效开发” 的正确姿势,小白赶紧动手试试吧!

http://www.dtcms.com/a/390718.html

相关文章:

  • MySQL三范式详细解析
  • GitHub 仓库权限更改
  • 卷积神经网络(CNN)核心知识点总结
  • Python数据挖掘之基础分类模型_朴素贝叶斯
  • 数字工业化的终极形态:人、机器与算法的三重奏
  • [x-cmd] 在 Linux 与 MacOS 安装与使用 x-cmd
  • wkhtmltopdf 命令参数及作用大全
  • Windows路径转换成Cygwin中的Unix路径的方法
  • JavaWeb之Web资源与Servlet详解
  • [视图功能8] 图表视图:柱状图、折线图与饼图配置实战
  • TDengine IDMP 基本功能——数据可视化(5. 表格)
  • ViTables 安装与 HDF5 数据可视化全指南
  • Python爬虫实战:研究Pandas,构建最新网游数据采集与智能推荐系统
  • 在.NET中实现RabbitMQ客户端的优雅生命周期管理及二次封装
  • .NET自定义数据操作日志
  • 从“连不上网”到“玩转路由”:路由器配置与静态路由实战(小白也能轻松掌握)
  • R语言 生物信息如何解读geo数据集的说明,如何知道样本分类, MDA PCa 79(n = 3)n的含义
  • 你的第一个Node.js应用:Hello World
  • 【LVS入门宝典】LVS核心原理与实战:Real Server(后端服务器)高可用配置指南
  • TPAMI 25 ICML 25 Oral | 顶刊顶会双认证!SparseTSF以稀疏性革新长期时序预测!
  • rep()函数在 R 中的用途详解
  • 在Windows中的Docker与WSL2的关系,以及与WSL2中安装的Ubuntu等其它实例的关系
  • 编辑器Vim
  • 数字推理笔记——基础数列
  • 如何使用 FinalShell 连接本地 WSL Ubuntu
  • Node.js 进程生命周期核心笔记
  • 低空网络安全防护核心:管理平台安全体系构建与实践
  • 站内信通知功能websoket+锁+重试机制+多线程
  • Vue 3 <script setup> 语法详解
  • Redis三种服务架构详解:主从复制、哨兵模式与Cluster集群