Spring Boot 测试:单元、集成与契约测试全解析

打印 上一主题 下一主题

主题 987|帖子 987|积分 2961

一、Spring Boot 分层测试计谋

Spring Boot 应用采用经典的分层架构,差别层级的功能模块对应差别的测试计谋,以确保代码质量和系统稳定性。
Spring Boot 分层架构:
  1. Spring Boot分层架构   
  2. A[客户端] -->|HTTP 请求| B[Controller 层]   
  3. B -->|调用| C[Service 层]   
  4. C -->|调用| D[Repository 层]  
  5. D -->|操作| E[数据库]   
  6. E -->|调用| F[外部服务接口]
复制代码
分层测试计谋:

测试计谋焦点原则:
•单元测试 (UT)
隔离验证单模块逻辑(Controller、Service、Repository)。
价值:快速反馈,精准定位代码缺陷。
•集成测试 (IT)
垂直集成测试(应用内全链路)与程度集成测试(跨服务交互)
价值:包管生产环境行为一致性。
•契约测试 (CT)
保障跨服务接口一致性,与程度集成测试互补。
价值:防止接口“暗坑”,提升协作服从。
二、单元测试:逐层击破,精准验证

单元测试专注于验证单一模块的逻辑,通过模仿其依靠项,快速获取反馈。
2.1 Controller 层:HTTP接口的靶向验证
测试目的: REST API 接口的独立测试,隔离业务逻辑与外部依靠。
测试工具
•@WebMvcTest:轻量级切片测试,仅加载 Controller 层干系 Bean。
•MockMvc:模仿 HTTP 请求与响应,支持链式断言。
•@MockBean:Mock 依靠的 Service 层组件,隔离Service层依靠。
实战示例
  1. @WebMvcTest(UserController.class) //只加载UserController进行测试。
  2. public class UserControllerTest {
  3.     @Autowired
  4.     private MockMvc mockMvc;
  5.     //模拟UserService,用于提供预定义的行为。
  6.     @MockBean
  7.     private UserService userService;
  8.     @Test
  9.     void getUserById_Returns200() throws Exception {
  10.         // 模拟 Service 层返回
  11.         when(userService.findById(1L)).thenReturn(new User(1L, "Test"));
  12.         // 发起请求并断言
  13.         mockMvc.perform(get("/users/1"))
  14.                .andExpect(status().isOk())
  15.                .andExpect(jsonPath("$.name").value("Test"));
  16.     }
  17. }
复制代码
1.代码解析
•1L - 代表Long范例的 ID,符合User实体类的定义。
•/users/1 -为 HTTP 请求中的路径参数,Spring 会自动将其转换为Long范例。
•测试逻辑 -通过模仿UserService返回固定的数据,验证 Controller 层的输入输出行为。
2.注解解析
@WebMvcTest
•专注于Web 层的单元测试。只加载 Web 层干系的 Bean,如MockMvc。
•@WebMvcTest(UserController.class),表示只加载UserController进行测试。
@MockBean
•模仿服务层或其他依靠,避免与外部服务实际交互。
@Test
•标识一个单元测试方法。JUnit 会自动实行标志的方法,并报告效果。
MockMvc
•模仿 HTTP 请求并测试 Controller 行为及断言效果。
2.2 Service 层:业务逻辑深度验证
测试目的:验证业务规则的正确性、事件管理的行为符合预期。
测试工具
@MockBean + @SpringBootTest(轻量模式)
@MockBean模仿数据库操纵,结合@SpringBootTest提供的 Spring 应用上下文,进行Service层单元测试。
实战示例
  1. @SpringBootTest  // 启动一个完整的 Spring 应用上下文
  2. public class UserServiceTest {
  3.     // 自动注入 UserService 实例
  4.     @Autowired private UserService userService;
  5.    
  6.     // 创建一个模拟的 UserRepository Bean,替代真实的数据库操作
  7.     @MockBean
  8.     private UserRepository userRepository;
  9.    
  10.     @Test
  11.     void createUser_ValidInput_ReturnsUser() {
  12.         // 1. 准备测试数据
  13.         User user = new User("SpringBot");
  14.         when(userRepository.save(user)).thenReturn(user);
  15.         // 2. 调用业务方法
  16.         User result = userService.createUser(user);
  17.         // 3. 验证业务逻辑
  18.         assertThat(result.getName()).isEqualTo("SpringBot");
  19.         verify(userRepository).save(user); // 验证 Repository 方法被调用
  20.     }
  21.     @Test
  22.     void createUser_NullName_ThrowsException() {
  23.         // 验证业务规则:用户名为空时抛出异常
  24.         User user = new User(null);
  25.         assertThatThrownBy(() -> userService.createUser(user))
  26.             .isInstanceOf(IllegalArgumentException.class)
  27.             .hasMessage("用户名不能为空");
  28.     }
  29. }
复制代码
代码解析
•@SpringBootTest
启动 Spring Boot雷同真实的测试环境,加载整个应用上下文。通常用于集成测试。
与其他注解结合利用时,可用于单元测试。如结合@Autowired自动注入 Bean,或者@MockBean模仿服务进行单元测试。
•@Autowired
自动注入userService,用于测试业务逻辑。
•@MockBean
创建一个模仿的userRepository,替换真实的数据库操纵。
2.3 Repository 层:数据操纵底子校验
测试目的:验证JPA实体映射、底子查询逻辑。
工具:@DataJpaTest 默认利用内存数据库H2。
实战示例
  1. @DataJpaTest  // 启动 JPA 相关的测试环境,通常用于测试 Repository 层
  2. public class UserRepositoryTest {
  3.     @Autowired private TestEntityManager entityManager;  // 用于与数据库进行交互,执行持久化操作
  4.     @Autowired private UserRepository userRepository;  // 自动注入 UserRepository,用于测试数据访问方法
  5.     @Test  // 标记为测试方法
  6.     void findByEmail_ExistingEmail_ReturnsUser() {
  7.         // 创建一个用户对象并持久化到数据库
  8.         User user = new User("test@example.com");
  9.         entityManager.persist(user);
  10.         // 调用 UserRepository 方法,根据 email 查找用户
  11.         User found = userRepository.findByEmail("test@example.com");
  12.         // 断言返回的用户对象不为 null
  13.         assertThat(found).isNotNull();
  14.     }
  15. }
复制代码
关键点:
•TestEntityManager 手动管理测试数据。
•默认隔离真实数据库,确保快速实行。
单元测试的上风:
•快速实行,约 50 毫秒/测试。
•精准定位问题。
三、集成测试:全链路一致性包管

3.1 垂直集成测试(应用内全链路)
测试目的:验证应用内各层的完整调用链。
工具组合
•@SpringBootTest:启动 Spring Boot 应用测试环境,进行全链路集成测试。
•@Testcontainers:通过 Docker 启动真实数据库容器(如 PostgreSQL)。
•@AutoConfigureMockMvc:自动设置MockMvc,用于模仿 HTTP 请求。
•@Container:定义 Testcontainers 容器,启动真实数据库实例。
•OrderRepository:验证数据是否已保存至数据库。
代码示例
  1. @SpringBootTest
  2. @AutoConfigureMockMvc
  3. @Testcontainers
  4. public class OrderIntegrationTest {
  5.     @Autowired
  6.     private MockMvc mockMvc; // 模拟 HTTP 请求
  7.     @Autowired
  8.     private OrderRepository orderRepository;  // 注入 Repository 层以验证数据库
  9.     @Container
  10.     public static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:latest")
  11.             .withDatabaseName("testdb")
  12.             .withUsername("test")
  13.             .withPassword("password");
  14.     @Test
  15.     void createOrder_ValidRequest_OrderIsSaved() throws Exception {
  16.         // 发送请求创建订单
  17.         mockMvc.perform(post("/orders")
  18.                .contentType(MediaType.APPLICATION_JSON)
  19.                .content("{ "productId": 1 }"))
  20.                .andExpect(status().isCreated());
  21.         // 验证数据库中是否有保存的订单
  22.         Order order = orderRepository.findByProductId(1);
  23.         assertThat(order).isNotNull();
  24.         assertThat(order.getProductId()).isEqualTo(1);
  25.     }
  26. }
复制代码
3.2 程度集成测试(跨服务交互)
测试目的:验证与外部服务的真实交互(如支付网关),确保跨服务的协议兼容性。
工具组合
•@SpringBootTest
•@Testcontainers:启动模仿的外部服务容器(如 WireMock)。
•WireMockServer:模仿外部服务的响应,进行服务间的交互测试。
•@BeforeAll / @AfterAll:在测试实行前后设置和清理模仿服务。
代码示例
  1. @SpringBootTest
  2. @Testcontainers
  3. public class PaymentServiceIntegrationTest {
  4.     @Autowired
  5.     private PaymentService paymentService;
  6.     @Container
  7.     public static WireMockServer wireMockServer = new WireMockServer(options().port(8089));  // 设置外部服务模拟
  8.     @BeforeAll
  9.     static void setup() {
  10.         wireMockServer.start();
  11.         configureFor("localhost", 8089);
  12.         stubFor(post(urlEqualTo("/payment"))
  13.                 .willReturn(aResponse()
  14.                         .withStatus(200)
  15.                         .withBody("{"status": "success"}")));
  16.     }
  17.     @AfterAll
  18.     static void teardown() {
  19.         wireMockServer.stop();
  20.     }
  21.     @Test
  22.     void processPayment_ValidRequest_ReturnsSuccess() {
  23.         // 模拟支付服务调用
  24.         PaymentRequest paymentRequest = new PaymentRequest(1, 100);
  25.         PaymentResponse response = paymentService.processPayment(paymentRequest);
  26.         // 验证支付处理是否成功
  27.         assertThat(response.getStatus()).isEqualTo("success");
  28.     }
  29. }
复制代码
解析
•WireMockServer:模仿外部支付服务。
•PaymentService:调用外部支付服务并验证支付效果。
3.3 长期层的集成测试
测试目的:验证应用与真实数据库、中心件的交互逻辑。
工具组合
•Testcontainers:启动真实数据库(如MySQL、PostgreSQL)。
•@DynamicPropertySource:动态注入测试环境设置。
•@DataJpaTest:聚焦 JPA 层测试,自动设置 H2 或真实数据库。
实战示例
  1. @Testcontainers  // 启动容器化的数据库实例(这里使用 PostgreSQL)
  2. @DataJpaTest  // 启动 JPA 测试环境,只加载与 JPA 相关的配置。
  3. @AutoConfigureTestDatabase(replace = NONE)  // 禁用 Spring Boot 默认的内存数据库配置,使用实际的 PostgreSQL 容器
  4. public class UserRepositoryIntegrationTest {
  5.     @Container
  6.     static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");  // 启动 PostgreSQL 容器,使用官方 15 版本
  7.     @DynamicPropertySource  // 动态配置数据库连接属性
  8.     static void configure(DynamicPropertyRegistry registry) {
  9.         registry.add("spring.datasource.url", postgres::getJdbcUrl);  // 配置数据库连接 URL
  10.         registry.add("spring.datasource.username", postgres::getUsername);  // 配置数据库用户名
  11.         registry.add("spring.datasource.password", postgres::getPassword);  // 配置数据库密码
  12.     }
  13.     @Test
  14.     void saveUser_PersistsToRealDatabase() {
  15.         // 创建用户并保存到数据库
  16.         User user = new User("IntegrationTest");
  17.         userRepository.save(user);
  18.         // 断言数据库中保存的用户数量为 1
  19.         assertThat(userRepository.findAll()).hasSize(1);
  20.     }
  21. }
复制代码
1.注解解析
@DataJpaTest
•专注于 JPA 层(JPA repository 或数据访问层操纵)的测试,自动设置一个嵌入式数据库并扫描@Entity类。
@AutoConfigureTestDatabase(replace = NONE)
•禁用默认的嵌入式数据库(如 H2),利用外部数据库(如 PostgreSQL容器)进行测试。
@Container
•标志一个静态的、全局共享的容器实例,为测试提供服务。
@DynamicPropertySource
•动态设置 Spring 环境的属性,常用于设置容器生成的数据库连接信息。
上风:真实数据库行为模仿,避免H2与生产数据库的差异问题。
四、契约测试:消费者驱动的接口保卫者

契约测试(Consumer-Driven Contract,CDC)用于确保服务提供者与消费者对接口的理解一致,防止因接口变更引发故障。
4.1 焦点流程
  1. participant Consumer as 消费者
  2. participant PactBroker as Pact Broker
  3. participant Provider as 提供者
  4. Consumer->>PactBroker: 1. 定义并发布契约
  5. PactBroker->>Provider: 2. 通知契约变更
  6. Provider->>PactBroker: 3. 验证实现是否符合契约
  7. PactBroker->>Consumer: 4. 反馈验证结果
复制代码
4.2 技术组合
•Pact:定义消费者盼望的接口契约
•@PactTestFor:绑定契约与测试用例
•Pact Broker:集中管理契约版本
4.3 实战示例
1.消费者端定义契约
  1. // OrderService(消费者端)定义契约
  2. @Pact(consumer = "OrderService", provider = "PaymentService")
  3. public RequestResponsePact paymentSuccessPact(PactDslWithProvider builder) {
  4.     return builder
  5.         // 提供者状态:订单已创建,待支付(需在提供者端实现数据准备)
  6.         .given("订单已创建,待支付")
  7.         // 消费者请求描述
  8.         .uponReceiving("支付订单的请求")
  9.         .method("POST")
  10.         .path("/payments")
  11.         .headers("Content-Type", "application/json") // 必须声明请求头
  12.         .body(new PactDslJsonBody()
  13.             .integerType("orderId", 1001)  // 订单ID为整数类型
  14.             .decimalType("amount", 299.99) // 金额为小数类型
  15.         )
  16.         // 提供者预期响应
  17.         .willRespondWith()
  18.         .status(200)
  19.         .headers(Map.of("Content-Type", "application/json")) // 响应头校验
  20.         .body(new PactDslJsonBody()
  21.             .stringType("status", "SUCCESS") // 状态必须为字符串且值=SUCCESS
  22.             .stringType("transactionId", "TX123456") // 交易ID必须为字符串
  23.         )
  24.         .toPact(); // 生成Pact契约文件
  25. }
复制代码
2.消费者端基于契约测试
  1. @Test
  2. @PactTestFor(
  3.     pactMethod = "paymentSuccessPact",
  4.     providerName = "PaymentService", // 指定提供者名称
  5.     pactVersion = PactSpecVersion.V3 // 使用Pact协议V3
  6. )
  7. void testPayment_WhenValidRequest_ReturnsSuccess(MockServer mockServer) {
  8.     // 1. 创建HTTP客户端,指向MockServer(模拟的PaymentService)
  9.     WebClient client = WebClient.create(mockServer.getUrl());
  10.     // 2. 构造请求并发送
  11.     PaymentRequest request = new PaymentRequest(1001, 299.99);
  12.     PaymentResponse response = client.post()
  13.         .uri("/payments")
  14.         .contentType(MediaType.APPLICATION_JSON)
  15.         .bodyValue(request)
  16.         .retrieve()
  17.         .bodyToMono(PaymentResponse.class)
  18.         .block(); // 同步等待响应
  19.     // 3. 断言响应符合契约
  20.     assertThat(response).isNotNull();
  21.     assertThat(response.getStatus()).isEqualTo("SUCCESS");
  22.     assertThat(response.getTransactionId()).isEqualTo("TX123456");
  23. }
复制代码
3.提供者端验证契约
目的:验证 PaymentService 的实现是否符合消费者定义的契约。
Step1. 提供者端代码实现
  1. // PaymentService(提供者端)的Controller实现
  2. @RestController
  3. public class PaymentController {
  4.     @PostMapping("/payments")
  5.     public ResponseEntity<PaymentResponse> processPayment(
  6.         @RequestBody PaymentRequest request
  7.     ) {
  8.         // 业务逻辑:处理支付请求
  9.         PaymentResponse response = new PaymentResponse();
  10.         response.setStatus("SUCCESS");
  11.         response.setTransactionId("TX" + UUID.randomUUID().toString().substring(0, 6));
  12.         return ResponseEntity.ok(response);
  13.     }
  14. }
复制代码
Step2. 提供者端 Pact 验证设置(build.gradle)
// 添加Pact验证插件
  1. plugins {
  2.     id "au.com.dius.pact" version "4.6.8"
  3. }
  4. dependencies {
  5.     // Pact提供者端依赖
  6.     testImplementation 'au.com.dius.pact.provider:junit5:4.6.8'
  7. }
复制代码
// 设置Pact验证任务
  1. pact {
  2.     serviceProviders {
  3.         PaymentService { // 提供者名称(需与契约中的provider一致)
  4.             protocol = 'http'
  5.             host = 'localhost'
  6.             port = 8080 // 本地服务端口
  7.             // 定义契约来源(本地文件或Pact Broker)
  8.             hasPactWith('OrderService') {
  9.                 pactSource = file("path/to/OrderService-PaymentService.json")
  10.             }
  11.         }
  12.     }
  13. }
复制代码
Step3: 提供者端状态准备(State Handler)
// 实现契约中的 given(“订单已创建,待支付”)
  1. public class PaymentStateHandler {
  2.     @BeforeRequest("订单已创建,待支付")
  3.     public void setupOrderState(Map<String, Object> params) {
  4.         // 模拟订单已创建的数据库操作
  5.         Order order = new Order(1001, 299.99);
  6.         orderRepository.save(order);
  7.     }
  8. }
复制代码
Step4: 提供者端测试类
  1. @Provider("PaymentService") // 声明提供者名称
  2. @PactFolder("pacts") // 契约文件路径
  3. @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
  4. public class PaymentServiceContractTest {
  5.     @TestTemplate
  6.     @ExtendWith(PactVerificationSpringProvider.class)
  7.     void pactVerificationTestTemplate(PactVerificationContext context) {
  8.         context.verifyInteraction();
  9.     }
  10.     @BeforeEach
  11.     void before(PactVerificationContext context) {
  12.         // 设置服务状态处理器
  13.         context.setTarget(HttpTestTarget.fromUrl(new UrlParser().parse("http://localhost:8080")));
  14.     }
  15. }
复制代码
Step5: 实行验证命令
  1. # 在提供者端执行验证(确保服务已启动)
  2. ./gradlew pactVerify -Dpact.provider.version=1.0.0
复制代码
4.4.契约测试总结
通过上述步调,契约测试完整覆盖了消费者与提供者的协作流程:
1.消费者定义契约:明白接口预期行为。
2.消费者本地验证:通过MockServer模仿提供者。
3.提供者实现接口:按契约开发功能。
4.提供者验证契约:确保实现与契约一致。
契约测试上风
•解耦团队协作,契约即文档。
•自动检测接口变更引发的破坏性修改。
五、总结:构建测试体系

5.1 测试计谋全景图
  1. Spring Boot分层架构   
  2. A[客户端] -->|HTTP 请求| B[Controller 层]   
  3. B -->|调用| C[Service 层]   
  4. C -->|调用| D[Repository 层]  
  5. D -->|操作| E[数据库]
  6. E -->|调用| F[外部服务接口]   
  7.    
  8. 测试策略全景   
  9. 单元测试   
  10. B1[Controller 单元测试] -->|@WebMvcTest + MockMvc| B   
  11. C1[Service 单元测试] -->|@MockBean| C   
  12. D1[Repository 单元测试] -->|@DataJpaTest| D
  13. 集成测试   
  14. Int1[全链路调用] --> B --> C --> D --> |Testcontainers + 真实数据库| E   
  15. Int2[水平集成测试] --> F
  16. 契约测试   
  17. Contract1[消费者契约测试] -->|Pact 定义期望接口+本地验证| F   
  18. Contract2[提供者契约测试] -->|Pact 验证实现| F   
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

瑞星

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表