在软件开发的生命周期中,测试是确保代码质量和应用可靠性的关键环节。没有充分测试的应用就像没有安全网的走钢丝表演,随时可能因为一个小的改动而导致整个系统崩溃。 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=DEBUGspring.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 应用。你将学到身份认证、权限控制、密码加密、会话管理等一系列安全相关的知识,这些是保证应用安全可靠的关键。