SpringBoot整合JUnit:单元测试从入门到精通
开篇:为什么要学 JUnit?先解决一个痛点
你有没有过这样的经历:
写了一个UserService,里面有个getUserById
方法,为了测试它,得手动写个main方法,new 对象、调方法、打印结果;改一行代码后,又要重新跑main—— 既麻烦又容易漏测。
在软件开发中,单元测试是保证代码质量的重要手段。它能够:
✅ 及早发现Bug:在开发阶段发现问题,降低修复成本
✅ 确保代码质量:验证每个单元功能的正确性
✅ 支持重构:提供安全网,让重构更有信心
✅ 文档作用:测试用例本身就是最好的使用示例
SpringBoot 还专门整合了 JUnit,不用自己搭环境,开箱即用。
一、先搞懂:JUnit 和 SpringBoot Test 是什么关系?
- JUnit:
Java 官方
的单元测试框架,负责 “执行测试方法、判断结果是否正确”(比如断言assertEquals(5, user.getAge())); - SpringBoot Test:
SpringBoot
提供的测试 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.heima
或com.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。
总结:核心要点一句话记
- 依赖:加spring-boot-starter-test,不用自己配 JUnit;
- 注解:测试类加@SpringBootTest(包不对就加classes),测试方法加@Test;
- 注入:用@Autowired拿要测试的 Bean,不用手动 new;
- 断言:用Assertions判断结果是否正确,这是测试的核心。
JUnit 的本质是 “用代码测试代码”,写测试类虽然多花 5 分钟,但改代码后能一键验证,避免线上出问题 —— 这才是 “高效开发” 的正确姿势,小白赶紧动手试试吧!