微服务架构设计模式-第九-十章

第9章 微服务架构中的测试策略(上)

AI问答:
在越来越复杂的软件开发场景下,测试到底该怎么做。

我们一开始聊的是最基础的自动化测试概念。你提到了自动化测试、TDD和测试用例这三个词,我帮你理清了它们的关系。测试用例就是具体的验证点,比如输入什么数据、预期得到什么结果。自动化测试是用代码代替人工去执行这些测试用例,让机器自动跑。而TDD,也就是测试驱动开发,它本质上不是一种测试技术,而是一种开发流程,要求你先写一个必然会失败的测试用例,再写刚好能让它通过的代码,最后重构,也就是红绿重构循环。TDD的真正目的不是测试本身,而是通过测试来驱动你的代码设计。

接着你问到了AI Review和Vibe Coding下的测试。这代表了当前AI编程的新趋势。AI Review不是简单地把代码贴给ChatGPT,而是分三个层次:IDE插件级的实时审查、代码提交阶段的集成审查,以及最高阶的架构级审查。而对于Vibe Coding,也就是用自然语言驱动AI生成代码,最大的风险是AI产生的幻觉。所以测试策略要变种,核心思路是测试即提示词。具体做法有四层:让AI先生成单元测试,用契约测试防止AI偷偷改接口返回字段,用混沌测试故意制造异常来验证AI写的异常处理是否健壮,最后用AI原生的E2E工具用自然语言写测试剧本。整个工作流变成了让AI同时生成业务代码和测试,跑测试如果报错就把错误堆栈贴回去让AI修复,再把覆盖率报告给AI让它补充遗漏的分支。

然后你抛出了一个更具体的场景,基于工作流引擎如Camunda或Activiti的定制化开发怎么做自动化测试。这种项目比普通CRUD复杂得多,核心策略是分层测试,把流程引擎的逻辑和定制业务代码解耦。第一层是流程引擎核心逻辑测试,用引擎自带的测试框架在JUnit里嵌入轻量级引擎,直接驱动BPMN文件跑,验证流程分支和网关走向。第二层是定制化业务节点测试,对每个服务任务对应的Java类做独立单元测试,用Mockito模拟掉数据库和外部API。第三层是集成测试,把流程引擎、业务节点、数据库和外部依赖串起来验证端到端场景,关键是确保测试环境的事务能回滚不污染数据。第四层是AI时代的智能化测试,用AI工作流平台来编排测试智能体,自动生成测试数据并驱动流程,甚至用AI来校验另一个AI的回复语义是否正确。

最后你敏锐地指出了工作流测试里最头疼的问题,需要很多不同角色的账号,还涉及复杂的权限设计。这个问题确实能把测试团队逼疯,因为要为普通员工、主管、HR、财务各建账号还要来回切换登录。但在自动化测试体系里,解决方案绝对不是靠人工切换账号,而是采用模拟身份加权限注入的策略。最主流的做法是Mock认证上下文,在单元测试里用注解或代码直接把当前用户身份强行塞进安全上下文中,根本不走真实登录接口,不依赖数据库用户表。如果业务代码强依赖用户ID去查个性化数据,就配合数据驱动加参数化账号池,用注解在测试前把预设账号的业务数据灌入数据库。如果是做压力测试或全链路回归,根本不在乎权限,就直接在测试配置里关闭所有权限校验,专注验证流程本身会不会死锁。还有一种情况是,测试工作流网关条件里的权限表达式时,直接在测试中强行覆盖流程变量跳过权限判断。最关键的一条原则是,不要做登录态缓存,不要依赖外部认证中心,身份是注入的而不是登录来的,把认证和授权彻底从流程测试中剥离,能让测试代码减少百分之八十的冗余。同时要把权限测试和流程扭转测试分开,前者单独验证授权表达式,后者只关心任务流转是否正确。

整体来看,现代测试的核心思想已经变了,不再是先写代码再补测试,而是测试驱动一切,包括驱动代码设计、驱动AI生成的代码质量、驱动复杂工作流系统的稳定性验证。无论场景怎么变,分层解耦、模拟依赖、数据隔离这三板斧始终是底层逻辑。而AI的加入,让测试用例的生成和执行方式都发生了根本性的变化,测试不再仅仅是验证手段,更成为了与AI协作的一种提示方式。如果你的项目还在手工点来点去,其实可以从最核心的几条业务路径开始,先写成自动化用例,哪怕只有百分之二十的覆盖率,也能立刻感受到回归测试带来的安全感。

9.1 微服务架构中的测试策略概述

9.1.1 什么是测试

在本章中,我的重点是自动化测试。下面的内容,测试都指的是自动化测试。

维基百科对测试用例的定义:用于特定目标的一组测试输入、执行条件和预期结果,例如执行特定的程序路径或验证是否符合特定要求。

编写自动化测试

junit

43-自动化测试结构

自动化测试通常包括四个阶段:

  1. 设置环境:将被测系统以及其他相关元素组成的测试环境初始化为所需的状态。
  2. 执行测试:调用被测系统,例如,在被测试的类上调用一个方法
  3. 验证结果:对调用的返回结果和被测系统的状态进行判断。
  4. 清理环境:必要时清理测试环境。很多测试忽略了这个阶段,但是某些类型的测试,比如涉及数据库的测试可能需要在这个阶段将数据库的状态回滚到设置环境阶段前的初始状态

为了减少代码重复并简化测试,一个测试类可能会有一个在所有测试方法之前运行的初始化方法,以及在最后运行的清理方法。

使用模拟和桩进行测试

被测系统在运行时常常会依赖另一些系统。依赖的麻烦在于它们可能把测试复杂化,并减慢测试速度。例如,orderController 类调用 orderService,orderService依赖于许多应用程序服务和基础设施服务。

下图所示,解决方案是用测试替身(test double)来消除被测系统的依赖性。测试替身是一个对象,该对象负责模拟依赖项的行为。
44-测试替身
有两种类型的测试替身:桩(stub)和模拟(mock)。术语 桩 和 模拟 通常可以互换使用,经它们的行为略有不同。

测试的不同类型

本章中重点介绍用于验证应用程序或服务的功能的自动化测试。

  • 单元测试
  • 集成测试:验证服务是否可以与基础设施服务(如数据库)或其他应用程序服务进行交互
  • 组件测试:单个服务的验收测试
  • 端到端测试:整个应用程序的验收测试

这些测试类型的主要区别在于范围。对于像Java这样的面向对象的语言,测试的目标就是类。另一个极端是端到端测试,它验证整个应用程序的行为。在这两个极端的中间是组件测试,测试单个服务。

范围只是区分 测试类型的一种方式。另一种方法是使用测试象限。

使用测试象限进行分类
45-测试象限
测试象限按两个维度对测试进行分类:

  • 测试是面向业务还是面向技术
  • 测试的目标是协助开发还是寻找产品缺陷

测试象限定义了四种不同的测试类别:

  • Q1 协助开发/面向技术:案源和集成测试
  • Q2 协助开发/面向业务:组件和端到端测试
  • Q3 寻找产品缺陷/面向业务:易用性和探索性测试
  • Q4 寻找产品缺陷/面向技术:非功能性验收测试,如性能测试

使用测试金字塔知道测试工作
46-测试金字塔
测试金字塔的关键思想是:在金字塔中从下往上移动时,应该编写的测试越来越少。

9.1.2 微服务架构中的测试挑战

验证两个服务可以交互的一种方法是同时运行两个服务,调用触发通信的API,并验证它是否具有预期结果。这肯定会遇到集成的问题,但它基本上都是端到端的。测试的过程需要运行这些服务的许多其他依赖项。测试可能还需要调用复杂的高级功能,例如业务逻辑,即使目标只是测试相对较低级别的进程间通信。最好避免编写像这样的端到端测试。我们需要编写更快、更简单、更可靠的测试,理想情况下可以单独测试服务。解决方案是使用所谓的消费者驱动的契约测试(consumer-driven contract testing)。

消费者驱动的契约测试

:侧重于验证提供者API的参数定义是否符合消费者的期望。对于 REST 接口,契约测试将验证提供者程序实现的接口是否:

  • 具有预期的HTTP方法和路径
  • 接受预期的HTTP头部,如果有的话
  • 接受请求主体,如果有的话
  • 返回预期中的响应,包括状态代码、头部和主体

重要的是要记住,契约测试不会彻底测试提供者的业务逻辑。这是单元测试的工作。
47-测试套件
使用spring cloud的契约测试服务

企业级契约测试框架:spring cloud contract

https://github.com/spring-attic/spring-cloud-contract

48-契约测试服务
49-契约服务代码实现
针对消息传递API的消费者契约测试

REST客户端并不是唯一一种对提供者的API有期望的消费者。采用异步请求/响应通信方式的服务订阅了某领域事件,那么它也是消费者。它们使用其他服务的消息传递API,并对该API的定义做出假设。我们也必须为这些服务编写消费者契约测试。

Spring Cloud Contract 也支持这类基于消息传递方式交互的服务的测试。契约的结构以及如何进行测试取决于交互的类型。用于测试领域事件发布的契约会包含一个样例领域事件。对提供者测试时,提供者程序触发这个事件,并验证它是否与契约中的事件匹配。消费
者测试则会验证消费者是否可以处理该事件。在下一章中,我将描述这类测试的一个实例。

异步请求/响应交互的契约类似于HTTP契约。它由请求消息和响应消息组成。在提供者端测试时,会使用契约中的请求消息来调用API,并验证响应是否与契约中的响应匹配。在消费者端测试时,使用该契约来配置一个桩,用来模拟成一个订阅者,该订阅者侦听契约的请求消息并使用指定的响应进行回复。

9.1.3 部署流水线

50-流水线

9.2 为服务编写单元测试

51-两种类型的单元测试

  • 独立型单元测试:使用针对类的依赖性的模拟对象隔离测试类
  • 协作型单元测试:测试一个类及其依赖项

类的职责及其在架构中的角色决定了要使用的测试的类型。控制器和服务类通常使用独立型单元测试。领域对象(例如实体和值对象)通常使用协作型单元测试。
52-类的职责决定单元测试类型

9.2.1 为实体编写单元测试

public class OrderTest
private ResultWithEvents < Order > createResult;
private Order order;
@Before
public void setUp() throws Exception {
        CreateResult = Order.CreateOrder(CONSUMER_ ID, AJANTA_ID, CHICKEN_VINDALO0 LINE ITEMS);
        order = createResult.result;
        @Test
        public void shouldCalculateTotal()(
                assertEquals(CHICKEN VINDALO0 PRICE.multiply(CHICKEN VINDALOO QUANTITY),
                    order.getOrderTotal());
        }
}

9.2.2 为值对象编写单元测试

值对象是不可变的,因此它们往往易于测试。不必担心副作用。对值对象的测试通常会创建特定状态的值对象,调用其中一个方法,并对返回值进行断言。

public class MoneyTest
private final int M1 AMOUNT10;
private final int M2 AMOUNT15;
private Money ml = new Money(M1 AMOUNT);
private Money m2 = new Money(M2_AMOUNT);
验证两个Money对象可以相加
@Testpublic void shouldAdd() {
        assertEquals(new Money(M1 AMOUNT + M2 AMOUNT), ml.add(m2));
        验证Money对象可以与整数相乘
        @Testpublic void shouldMultiply() {
                int multiplier = 12;
                assertEquals(new Money(M2_AMOUNT * multiplier), m2.multiply(multiplier));
        }
}

9.2.3 为Saga编写单元测试

Saga(例如CreateOrderSaga类)会实现重要的业务逻辑,因此需要进行测试。它是一个持久化对象,向Saga参与方发送命令式消息并处理它们的回复。如第4章所述,CreateOrderSaga与多个服务交换命令式/回复消息,例如consumer Service和Kitchen Service。对此类的测试会创建一个Saga,并验证它是否将消息按预期的顺序发送给Saga参与方。你需要为正常执行的场景编写单元测试,你还必须为Saga回滚的各种场景编写测试,例如Saga参与方发回了失败的消息。

一种方法是编写使用真实数据库和消息代理以及桩服务的测试,以此来模拟各种Saga参与方。例如,Consumer Service的将订阅consumerService命令式消息通道并发回所需的消息。但使用这种方法编写的测试非常缓慢。一种更有效的方法是编写模拟与数据库和消息代理交互的类的测试。这样,我们就可以专注于测试Saga的核心职责。

注意:使用了eventuate tram saga测试框架编写
53-saga单元测试
@Test shouldCreateOrder() 方法测试正常的执行路径

@Test shouldRejectOrderDueToConsumerVerificationFailed()方法测试 Consumer Service 拒绝订单的场景

9.2.4 为领域服务编写单元测试

public class OrderServiceTest(
    private OrderService orderService; 
    private OrderRepository orderRepository; 
    private DomainEventPublisher eventPublisher; 
    private RestaurantRepository restaurantRepository; 
    private SagaManager < CreateorderSagaState > createorderSagaManager; private SagaManager < CancelordersagaData > cancelorderSagaManager; 
    private SagaManager < ReviseOrderSagaData > reviseOrderSagaManager;
    
    @Before
    public void setup(){
        orderRepository = mock(OrderRepository.class);
		eventPublisher = mock(DomainEventPublisher.class);
		restaurantRepository = mock(RestaurantRepository.class);
		createOrderSagaManager = mock(SagaManager.class);
		cancelOrderSagaManager = mock(SagaManager.class);
		reviseOrderSagaManager = mock(SagaManager.class);
        //创建注入了模拟依赖项的orderService
        orderService = new OrderService(...)
    }
    
    @Test
    public void shouldCreateOrder(){
        when(restaurantRepository.findById(AJANTA_ID).thenReturn(Optional.of(AJANTA_RESTAURANT)));
        order.setId(ORDER_ID);
        return order;
        
        verift(orderRepository).save(same(order));
    }

setUp()方法创建一个注人了模拟依赖项的OrderService。@Test shouldCreateOrder()方法验证OrderService。createOrder()是否调用OrderRepository来保存新创建的Order,发布OrderCreated事件,并创建CreateOrderSaga。

9.2.5 为控制器编写单元测试

诸如Order Service之类的服务通常具有一个或多个控制器,用于处理来自其他服务和API Gateway的HTTP请求。控制器类由一组请求处理程序方法组成。每个方法都实现一个RESTAPI端点。方法的参数表示来自HTTP请求的值,例如路径变量。它通常调用领域服务或存储库并返回响应对象。例如,OrderController调用OrderSer-vice和OrderRepository。控制器的有效测试策略是模拟服务和存储库的独立型单元测试。

spring mock mvc / rest assured mock mvc

这些框架使你能够测试HTTP请求路由以及java对象与JSON之间的转换,而无须进行真正的网络调用。

OrderControllerTest创建-个为OrderService和orderRepository注人Mockito模拟的控制器。每个测试都配置模拟,发出HTTP请求,验证响应是否正确,并可能验证控制器是否调用了模拟。

when(orderRepository.findById(1L)).thenReturn(Optional.of(CHICKEN_VINDALOO_ORDER_);
given().standaloneSetup(configureControllers(new OrderController(orderService,orderRepository))).when().get("/orders/1").then().statusCode(200);                                        

9.2.6 为事件和消息处理程序编写单元测试

public void shouldCreateMenu(){
    given().eventHandlers(orderEventConsumer.domainEventHandlers()).when()
        .aggregate("net...Restaurant",AJANTA_ID).publishes(new RestaurantMother.AJANTA_RESTAURANT_MENU)
        .then().verify(() -> {
        verify(orderService).createMenu(AJANTA_ID,new Restaurant(RestaurantMother.AJANTA_RESTAURANT_MENU))
    })
}

setUp()方法创建一个注人了模拟orderService的OrderEventConsumer。sho-uldCreateMenu()方法发布一个RestaurantCreated事件,并验证OrderEvent-Consumer是否调用了OrderService。createMenu()。OrderEventConsumerTest类和其他单元测试类的执行速度非常快。单元测试只需几秒钟即可完成。

第10章 微服务架构中的测试策略(下)

10.1 编写集成测试

54-集成测试

  • 测试每个服务的适配器,以及可能的适配器支持类
  • 使用契约,可以实现简化验证应用程序服务之间交互的目的,契约的结构取决于服务之间的交互类型
交互方式 消费者 提供者 契约
基于REST的请求/响应 API Gateway Order Sevrice HTTP请求与响应
发布/订阅 Order History Service Order Sevrice 领域事件
异步请求/响应 Order Sevrice Hitchen Sevrice 命令消息和回复消息

一个契约会包含一个或2个消息,例如,发布/订阅方式下,有一个消息,而在请求/响应或异步请求/响应模式下是两个消息。

契约用于测试消费者和提供者,确保它们就API达成一致。它们的使用方式略有不同,具体取决于你是在测试消费者还是提供者。

10.1.1 针对持久化层的集成测试

持久化集成测试每个阶段的行为如下:

  • 设置:通过创建数据库结构设置数据库,并将其初始化为已知状态。也可能开始执行一些必要的数据库事务
  • 执行:执行数据库状态
  • 验证:对数据库的状态和从数据库中检索的对象进行断言
  • 拆解:可选阶段,可以撤销对数据库所做的更改
@RunWith(SpringRunner.class)
@SpringBootTest(classes = OrderJpaTestConfiguration.class) 
public class OrderJpaTest
{
    @Autowired 
    private OrderRepository orderRepository; 
    @Autowired 
    private TransactionTemplate transactionTemplate; 
    
    @Test
    public void shouldSaveAndLoadOrder() {
            Long orderId = transactionTemplate.execute((ts) -> {
                        Order order = new Order(CONSUMER ID, AJANTA ID, CHICKEN VINDALOO LINE ITEMS);
                        orderRepository.save(order);
                        return order.getId();
            });
            transactionTemplate.execute((ts) -> (
                Order order = orderRepository.findById(orderId).get();
                assertEquals(OrderState.APPRoVAL PENDING, order.getState());
                assertEquals(AJANTA ID, order.getRestaurantId());
                assertEquals(CONsUMER_ID, order.getConsumerId().longValue());
                assertEquals(CHICKEN_VINDALO0_LINE_ITEMS, order.getLineItems());
                return null;
            });
    }
}

shouldSaveAndLoadOrder()测试方法执行两个事务。第一个事务在数据库中保存新创建的Order。第二个事务加载Order并验证其字段是否已正确初始化。

你需要解决的一个问题是如何配置在持久化集成测试中使用的数据库。在测试期间运行数据库实例的有效解决方案是使用Docker。

10.1.2 针对基于REST的请求/响应式交互的集成测试

55-基于REST请求的集成测试
wireMock

10.1.3 针对发布/订阅式交互的集成测试

服务通常会发布由一个或多个其他服务使用的领域事件。集成测试必须验证通过(发布)方及其消费(接收)方是否就消息通道和领域事件的结构达成一致。

契约还有两个重要元素:

  • label:用于消费者测试,触发spring conrtact 发布事件
  • triggeredBy:生成的测试方法调用的超类方法的名称,用于触发事件的发布

10.1.4 针对异步请求/响应式交互的集成契约测试

10.2 编写组件测试

10.2.1 定义验收测试

验收测试是针对软件组件的面向业务的测试。它们从组件客户端而不是内部实现的角度描述了所需的外部可见行为。这些测试源自用户故事或用例。

56-用户故事

Gherkin自然语言 DSL,写 feature 文本,无执行能力

CucumberBDD 测试框架,解析 Gherkin,绑定代码执行

每个场景都定义了一个验收测试。场景中的 given 对应的是测试的设置阶段,when 对应的是执行阶段,then 和 and 对应的是验证阶段。

10.2.2 使用Gherkin编写验收测试

使用Java编写验收测试是有挑战性的,用例定义的测试场景和Java编写的测试代码可能并不一致。高度抽象的场景和由底层Java编写的测试之间也可能脱节。此外,还存在一种风险,即场景缺乏精确性或模糊不清,无法转换为Java代码。更好的方法是消除手动转换步骤并编写可执行的场景。

Gherkin是用于编写可执行规范的DSL。使用Gherkin时,你可以使用类似英语的场景定义验收测试,例如之前显示的场景。然后使用Cucumber执行规范,Cucumber是Gherkin的测试自动化框架。Gherkin和Cucumber可以自动将场景转换为可运行的代码。

Order Service等服务的Gherkin规范包含一系列功能。每个功能都由一组场景描述,例如你之前看到的场景。情景具有given-when-then结构。given是先决条件,when是发生的动作或事件,then/and是预期的结果。

使用Cucumber 执行 Gherkin 的测试规范

Cucumber是一个自动化测试框架,用于执行用Gherkin编写的测试。它有多种语言版本,包括Java。使用Cucumber for Java时,你可以编写一个步骤定义类,如代码清单10-12所示。步骤定义类包含一组方法,这些方法定义了每个given-when-then步骤的具体含义。每个步骤定义方法都使用@Given、@when、@Before或@And进行注解。这些注解中的每一个都有一个值元素,它是一个正则表达式,Cucumber与步骤匹配。

每种类型的方法都是测试特定阶段的一部分:

  • @Given:设置阶段。
  • @When:执行阶段。
  • @Then 和 @And:验证阶段
    57-cucumberJava示例

10.2.3 设计组件测试

进程内组件测试

进程内组件测试使用常驻内存的桩和模拟代替其依赖性来运行服务。

进程外组件测试

把服务打包为生成环境就绪的格式,并将其作为单独的进程运行。

如何为进程外组件测试编写桩服务

Spring cloud contract

10.2.4 为FTGO的Order Service编写组件测试

docker

运行组件测试

10.3 端到端测试

10.3.1 设计端到端测试

编写用户旅程(user journey)测试。用户旅程测试对应于用户使用系统的过程。例如,你可以编写一个完成所有三项测试的单个测试。

10.3.2 编写端到端测试

gherkin & cucumber

10.3.3 运行端到端测试

docker compose

posted @ 2026-09-14 23:12  LHX2018  阅读(6)  评论(0)    收藏  举报