自动化测试(十) 微服务测试策略-单元到集成到契约到端到端分层实战
微服务测试策略单元→集成→契约→端到端分层实战前面咱们分别聊了单元测试、接口测试、契约测试。今天把它们串起来聊聊微服务架构下怎么设计完整的测试策略——每一层测什么、怎么测、用什么工具。一、微服务测试的金字塔变体单体应用的测试金字塔在微服务架构下要调整单体应用金字塔 微服务金字塔 /\ /\ / \ E2E少 / \ 端到端极少 /----\ /----\ ← 只验证核心链路 / \ 集成中 / \ 契约测试中 /--------\ /--------\ ← 服务间接口约定 / \ 单元多 / \ 集成单元多 /------------\ /------------\ ← 服务内部分层关键变化单元测试依然最多但增加了服务内集成测试契约测试成为新的一层验证服务间接口端到端测试更少因为涉及多个服务成本高、定位难二、分层实战从里到外假设咱们要测试一个订单服务它有这些组件┌─────────────────────────────────────────┐ │ OrderController │ ← REST API入口 │ POST /orders GET /orders/{id} │ ├─────────────────────────────────────────┤ │ OrderService │ ← 业务逻辑 │ createOrder() getOrder() │ ├─────────────────────────────────────────┤ │ OrderRepository │ ← 数据访问 │ Spring Data JPA │ ├─────────────────────────────────────────┤ │ 外部依赖 │ │ UserClient StockClient PayClient │ ← Feign/HTTP调用 └─────────────────────────────────────────┘第一层单元测试Unit Test测什么OrderService的业务逻辑不依赖Spring容器、不连数据库、不调外部服务。ExtendWith(MockitoExtension.class)classOrderServiceTest{MockOrderRepositoryorderRepository;MockUserClientuserClient;MockStockClientstockClient;MockPayClientpayClient;MockApplicationEventPublishereventPublisher;InjectMocksOrderServiceorderService;TestDisplayName(正常创建订单扣库存、算价格、发事件)voidshouldCreateOrderSuccessfully(){// GivenCreateOrderRequestrequestnewCreateOrderRequest(ITEM-001,2);when(userClient.getCurrentUser()).thenReturn(newUser(1L,Alice));when(stockClient.deduct(ITEM-001,2)).thenReturn(true);when(orderRepository.save(any())).thenAnswer(inv-{Orderorderinv.getArgument(0);order.setId(100L);returnorder;});// WhenOrderResultresultorderService.createOrder(request);// ThenassertThat(result.isSuccess()).isTrue();assertThat(result.getOrderId()).isEqualTo(100L);// 验证交互verify(stockClient).deduct(ITEM-001,2);verify(orderRepository).save(argThat(order-order.getTotalAmount().equals(newBigDecimal(199.98))));verify(eventPublisher).publishEvent(any(OrderCreatedEvent.class));}TestDisplayName(库存不足时订单创建失败)voidshouldFailWhenStockInsufficient(){when(userClient.getCurrentUser()).thenReturn(newUser(1L,Alice));when(stockClient.deduct(ITEM-001,100)).thenReturn(false);assertThatThrownBy(()-orderService.createOrder(newCreateOrderRequest(ITEM-001,100))).isInstanceOf(InsufficientStockException.class);verify(orderRepository,never()).save(any());}}单元测试的关键快毫秒级独立不依赖外部覆盖分支if/else、异常路径第二层切片测试Slice TestSpring Boot提供了WebMvcTest、DataJpaTest等注解只加载特定层的Bean。WebMvcTest只测Controller层WebMvcTest(OrderController.class)AutoConfigureMockMvcclassOrderControllerTest{AutowiredMockMvcmockMvc;MockBeanOrderServiceorderService;MockBeanJwtTokenProvidertokenProvider;AutowiredObjectMapperobjectMapper;TestDisplayName(创建订单接口返回201和订单ID)voidshouldReturn201WhenCreateOrder()throwsException{when(orderService.createOrder(any())).thenReturn(newOrderResult(true,100L,CREATED));mockMvc.perform(post(/api/orders).contentType(MediaType.APPLICATION_JSON).header(Authorization,Bearer valid-token).content(objectMapper.writeValueAsString(Map.of(sku,ITEM-001,quantity,2)))).andExpect(status().isCreated()).andExpect(jsonPath($.success).value(true)).andExpect(jsonPath($.orderId).value(100));}TestDisplayName(缺少必填参数返回400)voidshouldReturn400WhenMissingRequiredField()throwsException{mockMvc.perform(post(/api/orders).contentType(MediaType.APPLICATION_JSON).header(Authorization,Bearer valid-token).content({}))// 空body.andExpect(status().isBadRequest()).andExpect(jsonPath($.errors).isArray());}}WebMvcTest只加载Controller、ControllerAdvice、JsonComponentConverter、Filter、WebMvcConfigurer不加载Service、Repository、Component启动速度比SpringBootTest快得多。DataJpaTest只测Repository层DataJpaTestAutoConfigureTestDatabase(replaceAutoConfigureTestDatabase.Replace.NONE)TestcontainersclassOrderRepositoryTest{ContainerstaticMySQLContainer?mysqlnewMySQLContainer(mysql:8.0);DynamicPropertySourcestaticvoidconfigureProperties(DynamicPropertyRegistryregistry){registry.add(spring.datasource.url,mysql::getJdbcUrl);}AutowiredOrderRepositoryorderRepository;TestDisplayName(根据用户ID查询订单)voidshouldFindOrdersByUserId(){// GivenorderRepository.save(newOrder(1L,ITEM-001,newBigDecimal(99.99)));orderRepository.save(newOrder(1L,ITEM-002,newBigDecimal(199.99)));orderRepository.save(newOrder(2L,ITEM-003,newBigDecimal(50.00)));// WhenListOrderuser1OrdersorderRepository.findByUserId(1L);// ThenassertThat(user1Orders).hasSize(2);assertThat(user1Orders).extracting(Order::getSku).containsExactlyInAnyOrder(ITEM-001,ITEM-002);}TestDisplayName(复杂查询查询某用户的待支付订单总金额)voidshouldCalculatePendingAmount(){// 测试Query注解写的复杂SQLBigDecimaltotalorderRepository.calculatePendingAmountByUserId(1L);assertThat(total).isEqualByComparingTo(newBigDecimal(299.98));}}DataJpaTest只加载Entity、Repository嵌入式数据库默认或配置的数据源不加载Controller、Service第三层服务内集成测试Integration Test测整个服务但Mock外部依赖。SpringBootTest(webEnvironmentSpringBootTest.WebEnvironment.RANDOM_PORT)TestcontainersclassOrderServiceIntegrationTest{LocalServerPortintport;ContainerstaticMySQLContainer?mysqlnewMySQLContainer(mysql:8.0);RegisterExtensionstaticWireMockExtensionuserServiceMockWireMockExtension.newInstance().options(wireMockConfig().dynamicPort()).build();RegisterExtensionstaticWireMockExtensionstockServiceMockWireMockExtension.newInstance().options(wireMockConfig().dynamicPort()).build();DynamicPropertySourcestaticvoidconfigureProperties(DynamicPropertyRegistryregistry){registry.add(spring.datasource.url,mysql::getJdbcUrl);registry.add(services.user.url,userServiceMock::baseUrl);registry.add(services.stock.url,stockServiceMock::baseUrl);}AutowiredTestRestTemplaterestTemplate;AutowiredOrderRepositoryorderRepository;BeforeEachvoidsetUp(){orderRepository.deleteAll();// 配置下游MockuserServiceMock.stubFor(get(/api/users/current).willReturn(okJson({id:1,name:Alice})));stockServiceMock.stubFor(post(/api/stock/deduct).willReturn(okJson({success:true,remaining:98})));}TestDisplayName(完整流程创建订单并持久化到数据库)voidshouldCreateOrderAndPersist(){// GivenCreateOrderRequestrequestnewCreateOrderRequest(ITEM-001,2);// WhenResponseEntityOrderResultresponserestTemplate.postForEntity(/api/orders,request,OrderResult.class);// Then: HTTP响应assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);assertThat(response.getBody().isSuccess()).isTrue();// Then: 数据库验证ListOrderordersorderRepository.findAll();assertThat(orders).hasSize(1);assertThat(orders.get(0).getSku()).isEqualTo(ITEM-001);assertThat(orders.get(0).getStatus()).isEqualTo(OrderStatus.CREATED);// Then: 下游服务确实被调用了stockServiceMock.verify(postRequestedFor(urlEqualTo(/api/stock/deduct)));}}第四层契约测试Contract Test前面第5篇详细讲过这里总结在分层中的位置// 消费者端订单服务验证契约PactTestFor(providerNamestock-service)classStockServiceContractTest{Pact(consumerorder-service)RequestResponsePactstockDeductPact(PactDslWithProviderbuilder){returnbuilder.given(stock available).uponReceiving(deduct stock).method(POST).path(/api/stock/deduct).body(newPactDslJsonBody().stringType(sku,ITEM-001).integerType(quantity,2)).willRespondWith().status(200).body(newPactDslJsonBody().booleanType(success,true).integerType(remaining,98)).toPact();}}第五层端到端测试E2E Test只验证最核心的用户路径涉及多个真实服务。// 用Docker Compose启动所有服务然后跑E2E测试TestcontainersclassOrderE2ETest{ContainerstaticDockerComposeContainer?environmentnewDockerComposeContainer(newFile(docker-compose.test.yml)).withExposedService(order-service,8080).withExposedService(user-service,8080).withExposedService(stock-service,8080).withExposedService(mysql,3306).waitingFor(order-service,Wait.forHttp(/actuator/health).forStatusCode(200));TestDisplayName(用户下单完整流程)voidshouldCompleteOrderFlow(){StringorderServiceUrlhttp://environment.getServiceHost(order-service,8080):environment.getServicePort(order-service,8080);// 1. 用户注册// 2. 用户登录// 3. 浏览商品// 4. 创建订单// 5. 支付订单// 6. 验证订单状态given().baseUri(orderServiceUrl).body(createOrderRequest).when().post(/api/orders).then().statusCode(201).body(status,equalTo(PAID));}}三、各层的执行策略# CI流水线分层执行stages:-unit-tests# 每次提交都跑 1分钟-integration-tests# 每次提交都跑 5分钟-contract-tests# 每次提交都跑 2分钟-e2e-tests# 合并到main分支前跑 15分钟unit-tests:script:-mvn test-pl order-service# 只跑单元测试only:-merge_requests-mainintegration-tests:script:-mvn verify-Pintegration-test# 跑集成测试only:-merge_requests-maine2e-tests:script:-docker-compose-f docker-compose.test.yml up-d-mvn test-Pe2e-test-docker-compose-f docker-compose.test.yml downonly:-main# 只在main分支跑when:manual# 可手动触发四、小结今天咱们串起了微服务测试的完整分层层级范围工具速度频率单元测试Service/UtilJUnit Mockito毫秒每次提交切片测试Controller/RepositoryWebMvcTest / DataJpaTest秒每次提交集成测试整个服务SpringBootTest Testcontainers10秒每次提交契约测试服务间接口Pact / Spring Cloud Contract秒每次提交E2E测试多服务完整流程Docker Compose RestAssured分钟合并前一句话总结微服务测试不是测得越多越好而是每层测该测的。单元测逻辑、集成测协作、契约测约定、E2E验流程。