在软件开发的生命周期中,测试是确保代码质量和应用可靠性的关键环节。没有充分测试的应用就像没有安全网的走钢丝表演,随时可能因为一个小的改动而导致整个系统崩溃。 Spring Boot提供了强大的测试支持,让你能够编写各种类型的测试,从单元测试到集成测试,从Web层测试到数据层测试,全面覆盖应用的各个层面。
测试不仅仅是验证代码是否按预期工作,它还是文档的一种形式,能够清晰地表达代码的意图和行为。好的测试能够帮助开发者理解代码的功能,在重构时提供安全保障,在调试时快速定位问题。测试驱动开发(TDD)甚至将测试提升到了设计工具的高度,通过先编写测试来驱动代码的设计和实现。
Spring Boot的测试框架基于JUnit 5和Spring Test,提供了丰富的注解和工具来简化测试的编写。spring-boot-starter-test起步依赖已经包含了所有常用的测试框架,包括JUnit 5、Mockito、AssertJ、Hamcrest等,你不需要额外配置就能开始编写测试。这节课我们将深入学习如何为Spring Boot应用编写全面的测试,包括单元测试、集成测试、Web层测试、数据层测试等内容,确保应用的质量和可靠性。
在开始编写测试之前,我们需要理解测试金字塔的概念。测试金字塔将测试分为三个层次:单元测试位于底层,数量最多,运行最快;集成测试位于中间层,数量适中,运行速度中等;端到端测试位于顶层,数量最少,运行最慢。这种分层结构让你能够在测试覆盖率和执行效率之间找到平衡。
单元测试专注于测试单个组件或方法,通常不依赖外部资源,运行速度极快。集成测试验证多个组件之间的协作,可能需要启动Spring上下文或连接数据库,运行速度较慢。端到端测试验证整个应用的功能,从用户界面到数据库,运行速度最慢但最接近真实场景。
在实际项目中,你应该编写大量的单元测试来覆盖业务逻辑,编写适量的集成测试来验证组件协作,编写少量的端到端测试来验证关键业务流程。这种策略能够让你在保证测试覆盖率的同时,保持测试套件的快速执行,便于频繁运行测试并及时发现问题。

服务层通常包含核心的业务逻辑,是单元测试的重点。在编写服务层测试时,我们需要模拟依赖项,专注于测试业务逻辑本身,而不需要启动完整的Spring上下文。这种方式让测试运行更快,也更容易编写和维护。
让我们为CourseService编写单元测试。在src/test/java/com/example/myapp/my_spring_boot_app/service目录下创建CourseServiceTest.java文件。在IDE中,展开src/test/java目录,如果还没有对应的包结构,就逐级创建com.example.myapp.my_spring_boot_app.service包,然后在这个包中创建CourseServiceTest类:
|package com.example.myapp.my_spring_boot_app.service; import com.example.myapp.my_spring_boot_app.model.Course; import com.example.myapp.my_spring_boot_app.repository.CourseRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Arrays; import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class CourseServiceTest { @Mock private CourseRepository courseRepository; @InjectMocks private CourseService courseService; private Course testCourse; @BeforeEach void setUp() { testCourse = new Course(1L, "Spring Boot测试", "学习如何编写测试", "编程", 2); } @Test void testListCourses() { List<Course> courses = Arrays.asList(testCourse); when(courseRepository.findAll()).thenReturn(courses); List<Course> result = courseService.listCourses(); assertEquals(1, result.size()); assertEquals("Spring Boot测试", result.get(0).getTitle()); verify(courseRepository, times(1)).findAll(); } @Test void testGetCourseOrThrow_WhenCourseExists() { when(courseRepository.findById(1L)).thenReturn(Optional.of(testCourse)); Course result = courseService.getCourseOrThrow(1L); assertNotNull(result); assertEquals("Spring Boot测试", result.getTitle()); verify(courseRepository, times(1)).findById(1L); } @Test void testGetCourseOrThrow_WhenCourseNotExists() { when(courseRepository.findById(999L)).thenReturn(Optional.empty()); assertThrows(IllegalArgumentException.class, () -> { courseService.getCourseOrThrow(999L); }); verify(courseRepository, times(1)).findById(999L); } @Test void testCreateCourse_WithValidData() { when(courseRepository.save(any(Course.class))).thenReturn(testCourse); Course result = courseService.createCourse("Spring Boot测试", "学习如何编写测试", "编程", 2); assertNotNull(result); assertEquals("Spring Boot测试", result.getTitle()); verify(courseRepository, times(1)).save(any(Course.class)); } @Test void testCreateCourse_WithEmptyTitle() { assertThrows(IllegalArgumentException.class, () -> { courseService.createCourse("", "描述", "编程", 2); }); verify(courseRepository, never()).save(any(Course.class)); } @Test void testUpdateCourse_WhenCourseExists() { Course updatedCourse = new Course(1L, "更新的标题", "更新的描述", "编程", 3); when(courseRepository.findById(1L)).thenReturn(Optional.of(testCourse)); when(courseRepository.save(any(Course.class))).thenReturn(updatedCourse); Course result = courseService.updateCourse(1L, "更新的标题", "更新的描述", "编程", 3); assertEquals("更新的标题", result.getTitle()); assertEquals("更新的描述", result.getDescription()); verify(courseRepository, times(1)).findById(1L); verify(courseRepository, times(1)).save(any(Course.class)); } @Test void testDeleteCourse_WhenCourseExists() { when(courseRepository.existsById(1L)).thenReturn(true); doNothing().when(courseRepository).deleteById(1L); courseService.deleteCourse(1L); verify(courseRepository, times(1)).existsById(1L); verify(courseRepository, times(1)).deleteById(1L); } @Test void testDeleteCourse_WhenCourseNotExists() { when(courseRepository.existsById(999L)).thenReturn(false); assertThrows(IllegalArgumentException.class, () -> { courseService.deleteCourse(999L); }); verify(courseRepository, times(1)).existsById(999L); verify(courseRepository, never()).deleteById(anyLong()); } }
@ExtendWith(MockitoExtension.class)启用Mockito扩展,让你能够使用Mockito的注解。@Mock注解创建一个模拟对象,用于模拟依赖项。@InjectMocks注解创建一个真实对象,并将模拟的依赖项注入其中。@BeforeEach注解的方法在每个测试方法执行前运行,用于准备测试数据。
when().thenReturn()用于设置模拟对象的行为,当调用指定方法时返回指定的值。verify()用于验证方法是否被调用,以及调用的次数。assertThrows()用于验证是否抛出了预期的异常。这些工具让你能够全面测试服务的各种场景,包括正常流程和异常流程。
Repository层的测试通常需要真实的数据库连接,但我们可以使用内存数据库(如H2)来加速测试执行。Spring Boot Test提供了@DataJpaTest注解,它会自动配置内存数据库、JPA配置、以及事务管理,让你能够快速编写Repository测试。
在src/test/java/com/example/myapp/my_spring_boot_app/repository目录下创建CourseRepositoryTest.java文件:
|package com.example.myapp.my_spring_boot_app.repository; import com.example.myapp.my_spring_boot_app.model.Course; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; @DataJpaTest class CourseRepositoryTest { @Autowired
@DataJpaTest注解会启动一个轻量级的Spring上下文,只包含JPA相关的配置,不会加载完整的应用上下文。TestEntityManager是Spring Test提供的工具,用于在测试中操作实体,类似于JPA的EntityManager,但提供了更多测试友好的方法。每个测试方法都会在事务中执行,测试结束后会自动回滚,确保测试之间不会相互影响。
Web层测试验证控制器的行为,包括请求映射、参数绑定、响应生成等。Spring Boot Test提供了@WebMvcTest注解,它会启动一个只包含Web层的Spring上下文,不会加载服务层和数据层,让你能够专注于测试控制器逻辑。
在src/test/java/com/example/myapp/my_spring_boot_app/controller目录下创建CourseControllerTest.java文件:
|package com.example.myapp.my_spring_boot_app.controller; import com.example.myapp.my_spring_boot_app.model.Course; import com.example.myapp.my_spring_boot_app.service.CourseService; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import static org.mockito.ArgumentMatchers.any;
@WebMvcTest(CourseController.class)只加载指定的控制器和相关的Web配置,不会加载服务层和数据层。MockMvc是Spring Test提供的模拟MVC框架,用于模拟HTTP请求并验证响应。@MockBean创建一个模拟的Spring Bean,用于模拟服务层的依赖。
mockMvc.perform()用于执行HTTP请求,andExpect()用于验证响应。jsonPath()用于验证JSON响应的内容,使用JSONPath表达式来定位和验证JSON字段。这种方式让你能够全面测试控制器的各种场景,包括正常流程和异常流程。

集成测试验证多个组件之间的协作,通常需要启动完整的Spring上下文。Spring Boot Test提供了@SpringBootTest注解,它会启动完整的应用上下文,让你能够测试整个应用的功能。
在src/test/java/com/example/myapp/my_spring_boot_app目录下创建CourseIntegrationTest.java文件:
|package com.example.myapp.my_spring_boot_app; import com.example.myapp.my_spring_boot_app.model.Course; import com.example.myapp.my_spring_boot_app.repository.CourseRepository; import com.example.myapp.my_spring_boot_app.service.CourseService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; import java.util.HashMap; import java.util.Map;
@SpringBootTest启动完整的Spring Boot应用上下文,包括所有配置、Bean、以及自动配置。@AutoConfigureMockMvc自动配置MockMvc,让你能够测试Web层。@ActiveProfiles("test")激活test Profile,使用测试环境的配置。@Transactional确保每个测试方法在事务中执行,测试结束后自动回滚,保持数据库的干净状态。
这个集成测试验证了从创建课程到删除课程的完整生命周期,包括创建、查询列表、查询单个、更新、删除等操作。通过这种方式,你能够验证整个应用的功能是否正常工作,而不仅仅是单个组件。
响应式代码的测试与传统的阻塞式代码有所不同,因为响应式操作是异步的,需要使用特殊的工具来测试。Project Reactor提供了StepVerifier来测试响应式流,它能够验证数据流的元素、时序、以及完成状态。
在src/test/java/com/example/myapp/my_spring_boot_app/service目录下创建ReactiveCourseServiceTest.java文件:
|package com.example.myapp.my_spring_boot_app.service; import com.example.myapp.my_spring_boot_app.model.Course; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.time.Duration; class ReactiveCourseServiceTest { private ReactiveCourseService service = new ReactiveCourseService(); @Test void testFindCourseById() { Mono<Course> courseMono =
StepVerifier.create()创建一个验证器来测试响应式流。expectNextMatches()验证下一个元素是否满足指定的条件。expectNextCount()验证接下来会有指定数量的元素。verifyComplete()验证流正常完成。thenAwait()用于等待一段时间,这对于测试有时间延迟的流非常有用。
这种方式让你能够全面测试响应式流的行为,包括元素的顺序、数量、内容、以及完成状态。这对于确保响应式代码的正确性非常重要。
Spring Boot Test提供了测试切片(Test Slices)来优化测试性能。测试切片只加载应用的一部分,而不是完整的应用上下文,从而加快测试执行速度。我们已经看到了@WebMvcTest和@DataJpaTest的使用,它们都是测试切片的例子。
@WebMvcTest只加载Web层,适合测试控制器。@DataJpaTest只加载JPA配置,适合测试Repository。@JsonTest只加载JSON序列化相关的配置,适合测试JSON转换。@RestClientTest只加载REST客户端相关的配置,适合测试REST客户端。选择合适的测试切片能够显著提高测试执行速度,特别是在测试套件较大的情况下。
对于不需要Spring上下文的纯业务逻辑测试,应该避免使用任何Spring Test注解,直接使用JUnit和Mockito编写单元测试。这种方式运行最快,也最容易理解和维护。
测试环境通常需要与生产环境不同的配置,比如使用内存数据库而不是生产数据库,使用更简单的安全配置,禁用某些功能等。Spring Boot通过Profile机制让你能够为测试环境配置不同的参数。
在src/test/resources目录下创建application-test.properties文件:
|spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driver-class-name=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true logging.level.root=WARN logging.level.com.example.myapp=DEBUG
spring.jpa.hibernate.ddl-auto=create-drop让Hibernate在测试开始时创建表,测试结束后删除表,确保每次测试都从干净的状态开始。logging.level.root=WARN减少日志输出,加快测试执行速度。
在测试类中使用@ActiveProfiles("test")激活测试Profile,这样测试就会使用测试环境的配置,而不会影响生产环境的配置。
虽然内存数据库(如H2)对于大多数测试场景已经足够,但有时候你需要测试与真实数据库的交互,比如使用数据库特定的SQL特性。Testcontainers提供了在测试中使用真实数据库容器的能力,让你能够在Docker容器中运行MySQL、PostgreSQL等数据库。
在pom.xml中添加Testcontainers依赖:
|<dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>mysql</artifactId> <scope>test</scope>
创建使用Testcontainers的测试:
|@SpringBootTest @Testcontainers class CourseRepositoryWithTestcontainersTest { @Container static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); @DynamicPropertySource static void
@Testcontainers启用Testcontainers支持,@Container标记一个静态容器字段,Testcontainers会在测试类加载时启动容器,在所有测试完成后停止容器。@DynamicPropertySource用于动态设置配置属性,将容器的连接信息注入到Spring配置中。
这种方式让你能够在测试中使用真实的数据库,验证与数据库的交互是否正确,同时保持测试的隔离性和可重复性。

测试覆盖率是衡量测试质量的重要指标,它表示代码被测试覆盖的比例。虽然高覆盖率不能保证代码质量,但低覆盖率通常意味着代码缺乏测试。JaCoCo是Java生态系统中流行的代码覆盖率工具,Spring Boot可以轻松集成JaCoCo。
在pom.xml中添加JaCoCo插件:
|<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.11</version> <executions> <execution> <goals> <goal>prepare-agent</goal> </goals> </execution> <
运行mvn test后,JaCoCo会生成覆盖率报告,位于target/site/jacoco/index.html。打开这个文件,你可以看到每个类、每个方法的覆盖率,以及哪些代码没有被测试覆盖。
测试覆盖率应该作为质量指标之一,而不是唯一目标。100%的覆盖率并不意味着代码质量高,因为可能测试了代码但没有测试行为。你应该关注关键业务逻辑的覆盖率,确保重要的功能都有充分的测试。
编写可维护的测试与编写可维护的代码同样重要。好的测试应该易于理解、易于修改、易于扩展。遵循一些最佳实践能够帮助你编写更好的测试。
测试方法应该有一个清晰的名称,描述测试的场景和预期结果。例如,testCreateCourse_WithEmptyTitle_ThrowsException比testCreateCourse更清晰,它明确说明了测试的场景(空标题)和预期结果(抛出异常)。
测试应该遵循AAA模式:Arrange(准备)、Act(执行)、Assert(断言)。准备阶段设置测试数据和模拟对象,执行阶段调用被测试的方法,断言阶段验证结果。这种结构让测试更加清晰和易于理解。
测试应该独立,不依赖其他测试的执行顺序或结果。每个测试应该能够独立运行,测试之间不应该有共享状态。使用@BeforeEach和@AfterEach来准备和清理测试数据,确保测试的独立性。
测试应该快速执行,让你能够频繁运行测试并及时发现问题。避免在单元测试中使用真实的数据库连接、网络请求、文件系统操作等慢速操作,使用模拟对象来替代这些依赖。
测试代码也是代码,需要像生产代码一样认真对待。保持测试代码的简洁和清晰,定期重构测试代码,删除重复的测试,合并相似的测试。好的测试代码能够成为应用的最佳文档,帮助新团队成员快速理解代码的功能和行为。
现在你已经系统地掌握了为 Spring Boot 应用编写各种类型测试的“秘诀”。不管是单元测试、集成测试还是 Web 层测试,甚至是响应式代码的测试、测试切片的运用、测试环境的配置,还是如何写出可读、可维护的测试代码——你都已经可以轻松驾驭。
在下一个部分中,我们要一起进入另一个非常重要的话题:如何守护好我们的 Spring Boot 应用。你将学到身份认证、权限控制、密码加密、会话管理等一系列安全相关的知识,这些是保证应用安全可靠的关键。