使用 Citrus 测试 Quarkus

Citrus & Quarkus

本文介绍如何将 Quarkus 与 Citrus 测试框架结合使用,以编写面向事件驱动的应用的自动化测试。 Citrus 是一个开源的 Java 测试框架,专注于消息和集成测试。

开发人员可以轻松地将 @QuarkusTest 与 Citrus 的功能结合起来,以便在测试期间生成和消费事件。因此,测试可以通过与真实的通信交换事件和消息来与 Quarkus 面向事件驱动的应用进行交互。

介绍演示应用程序

在本文中,我们使用了一个名为 food-market 的 Quarkus 演示应用程序。您可以在 此 GitHub 代码仓库中找到演示应用程序代码和所有 Citrus 测试。该 Quarkus 应用连接到 Kafka 流作为面向事件驱动的应用,它生成和消费各种事件(例如,预订、供应、发货事件)。处理后的事件及其各自的状态存储在 PostgreSQL 数据库中。

Food Market App

food-market 应用会匹配传入的 bookingsupply 事件,并相应地生成 shippingbooking-completed 事件。

每个事件都引用一个产品,并在简单的 Json 对象结构中指定数量和价格。

{ "client": "citrus-test", "product": "Pineapple", "amount":  100, "price":  0.99 }

客户端创建 booking 事件,同时供应商将添加其各自的 supply 事件。Quarkus food-market 应用消费这两种事件类型,并找到匹配的预订和供应。一旦预订和供应在某些标准上匹配,该应用将生成 booking-completedshipping 事件作为结果。

最后但同样重要的是,预订客户端会通过电子邮件得知已完成的预订状态。

在一个完全自动化的集成测试中,我们希望通过与 Kafka 流进行真实的通信和数据库持久化来验证所有事件及其处理。

使用 Citrus 测试应用程序

Quarkus 应用连接到不同的基础设施(Kafka、PostgreSQL、Mail SMTP)。自动化集成测试应验证消息通信、事件处理以及与所有组件的连接。我们将使用 Citrus 测试框架,因为它提供了用于测试此类面向事件驱动的消息驱动解决方案的完整工具集。

首先要做的是将 Citrus 添加到 Quarkus 项目中。最方便的方法是导入 citrus-bom

Citrus BOM
<dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.citrusframework</groupId>
        <artifactId>citrus-bom</artifactId>
        <version>${citrus.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

citrus-quarkus 扩展提供了一个特殊的 @QuarkusTest 资源实现,使我们能够将 Citrus 与 Quarkus 测试结合起来。因此,让我们将此扩展添加为测试范围的依赖项。

Citrus Quarkus 扩展
<dependency>
  <groupId>org.citrusframework</groupId>
  <artifactId>citrus-quarkus</artifactId>
  <scope>test</scope>
</dependency>

此外,我们还需要包含其他一些 Citrus 模块,因为我们希望通过 Kafka 交换数据并连接到 PostgreSQL 数据库作为测试的一部分。Citrus 是模块化的。这意味着您可以从广泛的模块中进行选择,每个模块都为您的项目添加了特定的测试功能(例如,citrus-kafkacitrus-sqlcitrus-validation-json)。

在此示例项目中,我们将以下 Citrus 模块作为测试范围的依赖项包含在内

  • citrus-quarkus

  • citrus-kafka

  • citrus-validation-json

  • citrus-sql

  • citrus-mail

这样就完成了所有必需的 Citrus 模块的设置。现在我们可以开始编写自动化集成测试,以验证 Quarkus 面向事件驱动的应用。

在 QuarkusTest 之上编写 Citrus 测试

我们希望编写一个自动化测试,以确保所有入站事件(bookingsupply)都已正确处理,并且生成的出站事件(booking-completedshipping)符合预期。

Citrus test setup

Citrus 作为测试框架,将充当所有周边组件,生成客户端事件并验证生成的出站事件。此外,Citrus 将检查数据库以验证持久化的领域模型对象。稍后在更高级的测试场景中,Citrus 还将接收并验证 food-market Quarkus 应用发送的邮件内容。

现在,让我们从一个普通的 Quarkus 测试开始。测试需要启动 Quarkus 应用,还需要准备一些基础设施,例如数据库和 Kafka 流消息代理。幸运的是,Quarkus dev services 提供了出色的测试能力,可以自动启动代表所需基础设施的 Testcontainers。

该测试用 @QuarkusTest 注解进行标注。它启用了 Quarkus dev services 的测试能力,并负责为您设置所有内容。测试本身是任意的 JUnit Jupiter 单元测试,因此您可以从 Java IDE 或 Maven 测试生命周期中启动此测试。

现在让我们将 Citrus 加入进来。通过我们在上一节添加到 Maven 项目中的 Citrus Quarkus 扩展,我们现在可以为测试启用 Citrus 功能。只需将 @CitrusSupport 注解添加到测试类。

此注解为 Quarkus 测试启用了 Citrus 功能。Citrus 现在将参与 Quarkus 测试生命周期,使您能够注入特定的 Citrus 资源,例如端点以及 Citrus 测试运行器。

已启用 Citrus 的 Quarkus 测试
@QuarkusTest
@CitrusSupport
class FoodMarketApplicationTest {

    private final KafkaEndpoint bookings = kafka()
            .asynchronous()
            .topic("bookings")
            .build();

    @CitrusResource
    private TestCaseRunner t;

    @Inject
    ObjectMapper mapper;

    @Test
    void shouldProcessEvents() {
        Product product = new Product("Pineapple");

        Booking booking = new Booking("citrus-test", product, 100, 0.99D);
        t.when(send()
                .endpoint(bookings)
                .message().body(marshal(booking, mapper)));
    }
}

已启用 Citrus 的测试使用附加资源,例如名为 bookings 的 KafkaEndpointKafkaEndpoint 组件来自 citrus-kafka 模块,允许我们通过向主题发送和接收事件来与 Kafka 流进行交互。

Citrus TestCaseRunner 资源代表了 Citrus Java 特定领域测试语言的入口。这使我们能够在测试期间执行任何 Citrus 测试操作(例如,发送/接收消息、在 SQL 数据库中验证数据)。

查看此示例代码以向 Kafka 流主题发送消息。

发送预订事件
Product product = new Product("Pineapple");

Booking booking = new Booking("citrus-test", product, 100, 0.99D);
t.when(send()
    .endpoint(bookings)
    .message().body(marshal(booking, mapper)));

注入的 Citrus TestCaseRunner 能够使用 Gherkin Given-When-Then 语法并执行 Citrus 测试操作。此第一个测试活动在发送操作中引用 KafkaEndpoint bookings。测试能够使用领域模型对象(ProductBooking)作为消息正文。发送操作使用注入的 ObjectMapper 将领域模型对象正确序列化为 Json。

您也可以使用 @QuarkusIntegrationTest 注解来在单独的 JVM 中启动演示应用程序。这会将测试代码与应用程序分开,并且通常将测试绑定到 Maven 的集成测试阶段。请注意,集成测试无法注入应用程序资源,如 ObjectMapper 或 DataSource。好消息是,您也可以将相同的 Citrus 扩展与 @QuarkusIntegrationTest 一起使用。

这基本上就是如何在自动化集成测试中将 Citrus 功能与 Quarkus 测试 dev services 相结合。

其余的故事很简单。与发送预订事件的方式相同,我们现在也可以发送匹配的 supply 事件。

发送供应事件
Supply supply = new Supply(product, 100, 0.99D);
t.then(send()
    .endpoint(supplies)
    .message().body(marshal(supply)));

该测试现在已经生成了一个预订和一个匹配的供应事件。这应该会触发 food-market 应用生成相应的 booking-completedshipping 事件。作为测试的下一步,我们应该使用 Citrus 接收并验证这些事件。

接收并验证事件
class FoodMarketApplicationTest {

    // ... Kafka endpoints defined here

    @Test
    void shouldProcessEvents() {
        Product product = new Product("Pineapple");

        Booking booking = new Booking("citrus-test", product, 100, 0.99D);
        t.when(send()
            .endpoint(bookings)
            .message().body(marshal(booking, mapper)));

        // ... also send supply events

        ShippingEvent shippingEvent = new ShippingEvent(booking.getClient(), product.getName(), booking.getAmount(), "@ignore@");
        t.then(receive()
            .endpoint(shipping)
            .message().body(marshal(shippingEvent, mapper))
        );
    }
}

Citrus 能够在接收事件时执行强大的消息验证。这就是为什么我们在一开始就添加了 citrus-validation-json 模块。Citrus 中的 Json 消息验证器会将接收到的 Json 对象与预期的 Json 模板进行比较,并确保所有字段和属性都符合预期。

该测试会创建预期的 shippingEvent Json 对象,该对象使用 clientproductamount 等属性。接收到的事件必须与这些预期值匹配才能通过测试。不幸的是,我们无法验证 address 字段,因为它是由 Quarkus 应用生成的。这就是为什么使用 @ignored@ Citrus 验证表达式作为预期值时会忽略 address

Citrus Json 消息验证器功能非常强大,它将把接收到的发货事件与预期的 Json 对象进行比较。所有给定的 Json 属性都会被验证,并且在出现不匹配时测试将失败。

接收到的 Json
{ "client":  "citrus-test", "product": "Pineapple", "amount": 100, "address": "10556 Citrus Blvd." }
控制 Json
{ "client":  "citrus-test", "product": "Pineapple", "amount": 100, "address": "@ignore@" }

您可以在预期模板中使用忽略表达式、验证匹配器、函数和测试变量。

控制 Json
{ "client":  "${clientName}", "product": "@matches(Pineapple|Strawberry|Banana)@", "amount": "@isNumber()@", "address": "@ignore@" }

这样第一个测试就完成了,在测试中与被测应用交换了许多事件。现在让我们运行测试。

运行 Citrus 测试

示例中的 Quarkus 测试框架使用 JUnit Jupiter 作为测试驱动程序。这意味着您可以像运行任何其他 JUnit 测试一样,从您的 Java IDE 或使用 Maven 等工具来运行测试。

./mvnw test

测试现在使用 Maven 测试生命周期运行。@QuarkusTest dev services 将启动应用程序并使用 Testcontainers 准备基础设施。然后 Citrus 将生成事件并通过强大的 Json 验证来验证结果。

在第一个测试中,我们确保应用程序能够处理入站事件,并且生成的出站事件符合预期。现在,让我们进行更高级的测试,包括数据库和邮件服务器 SMTP 通信。

使用 SQL 验证存储的数据

在测试分布式事件驱动应用程序时,事件的时序是成功的关键要素。每个测试场景都旨在验证特定的应用程序行为,而事件的正确时序是触发和验证此行为的关键。时序对于避免运行到受比赛条件影响测试结果的flaky测试(例如 CI 作业)也很重要。

例如,假设测试需要先创建一个新产品,然后发送一个引用此新添加产品的预订事件。测试需要在发送预订事件之前等待产品事件完全处理。

在 Citrus 中,我们可以很容易地添加这个等待状态。

等待对象在持久化层中创建
Product product = new Product("Watermelon");
t.when(send()
    .endpoint(products)
    .message().body(marshal(product)));

t.then(repeatOnError()
    .condition((i, context) -> i > 25)
    .autoSleep(500)
    .actions(
        sql().dataSource(dataSource)
            .query()
            .statement("select count(id) as found from product where product.name='%s'"
                    .formatted(product.getName()))
            .validate("found", "1"))
);

发送产品事件后,我们使用 repeatOnError() 测试操作。结合 autoSleep 和最大重试计数设置,该操作会定期轮询数据库以查找创建的产品。这确保了在我们继续测试之前,新产品已正确存储到数据库中。

Citrus 中的数据库交互随 citrus-sql 模块提供,使您能够验证任何 SQL 结果集。

Quarkus 能够注入用于连接 PostgreSQL 数据库的 dataSource。当 Quarkus 在测试中使用 PostgreSQL Testcontainers 基础设施时,这也适用。只需在您的测试中使用 @Inject 注解,并在 Citrus sql() 测试操作中引用数据源。
您可以为常见的 Citrus 测试逻辑引入测试行为,例如等待领域模型对象在数据库中持久化。通常,测试行为将一组 Citrus 测试操作封装到一个可重用的实体中,您可以在测试中多次引用该实体。
Citrus 测试行为
public class WaitForProductCreated implements TestBehavior {

    private final Product product;
    private final DataSource dataSource;

    public WaitForProductCreated(Product product, DataSource dataSource) {
        this.product = product;
        this.dataSource = dataSource;
    }

    @Override
    public void apply(TestActionRunner t) {
        t.run(repeatOnError()
            .condition((i, context) -> i > 25)
            .autoSleep(500)
            .actions(
                sql().dataSource(dataSource)
                    .query()
                    .statement("select count(id) as found from product where product.name='%s'"
                            .formatted(product.getName()))
                    .validate("found", "1"))
        );
    }
}

在测试中,您可以应用测试行为。

应用测试行为
Product product = new Product("Watermelon");
t.when(send()
    .endpoint(products)
    .message().body(marshal(product)));

t.then(t.applyBehavior(new WaitForProductCreated(product, dataSource)));

能够查看数据库以检查持久化的实体非常强大,因为它允许我们完全控制测试流程。我们还可以使用 Citrus SQL 结果集验证来验证预订状态。

验证预订状态为已完成
t.then(sql().dataSource(dataSource)
        .query()
        .statement("select status from booking where booking.id='${bookingId}'")
        .validate("status", "COMPLETED")
);

这验证了具有给定 ID 的预订状态为 COMPLETED。Citrus 中的 SQL 结果集验证能够处理具有多行的复杂列集。

验证邮件服务器交互

被测 food-market Quarkus 应用可以通过电子邮件通知客户端预订已完成。

邮件内容
Subject: Booking completed!

Hey citrus-client, your booking Pineapple has been completed!

Citrus 测试能够通过启动一个 SMTP 邮件服务器来验证此特定邮件内容,该服务器将接收邮件并验证其内容。

在 Quarkus 中,我们可以使用 quarkus-mailer 扩展通过 SMTP 发送邮件。

Quarkus 邮件服务
@Singleton
public class MailService {

    @Inject
    ReactiveMailer mailer;

    public void send(Booking booking) {
        if (Booking.Status.COMPLETED != booking.getStatus()) {
            return;
        }

        mailer.send(
            Mail.withText("%s@quarkus.io".formatted(booking.getClient()),
                "Booking completed!",
                "Hey %s, your booking %s has been completed.".formatted(booking.getClient(), booking.getProduct().getName())
            )
        ).subscribe().with(success -> {
            // handle mail sent
        }, failure -> {
            // handle mail error
        });
    }
}

为了进行测试,Citrus 启动了一个 SMTP 邮件服务器,该服务器能够接收 Quarkus 发送的邮件。

Citrus 邮件服务器组件
@BindToRegistry
private MailServer mailServer = mail().server()
            .port(2222)
            .knownUsers(Collections.singletonList("foodmarket@quarkus.io:foodmarket:secr3t"))
            .autoAccept(true)
            .autoStart(true)
            .build();

让我们告诉 Quarkus 在测试期间连接到这个 Citrus 邮件服务器。

Quarkus mailer 配置
quarkus.mailer.mock=false
quarkus.mailer.own-host-name=localhost
quarkus.mailer.from=foodmarket@quarkus.io
quarkus.mailer.host=localhost
quarkus.mailer.port=2222

quarkus.mailer.username=foodmarket
quarkus.mailer.password=secr3t

通过此设置,我们现在可以添加一个接收和验证已发送邮件消息的测试操作。

验证已发送邮件消息
t.variable("client", "citrus-test");
t.variable("product", product.getName());

t.run(receive()
    .endpoint(mailServer)
    .message(MailMessage.request("foodmarket@quarkus.io", "${client}@quarkus.io", "Booking completed!")
            .body("Hey ${client}, your booking ${product} has been completed.", "text/plain"))
);

t.run(send()
    .endpoint(mailServer)
    .message(MailMessage.response(250, "Ok"))
);

预期的邮件内容使用了一些测试变量 ${client}${product}。您可以相应地在 Citrus 中设置这些测试变量,以便在执行验证之前解析这些占位符。

邮件服务器根据 SMTP 协议响应一个代码和一个文本。在成功的情况下,这是 250 Ok 响应。

同样,您可以引入一个 Citrus 测试行为来涵盖预订完成邮件消息的验证。许多测试可以在其测试逻辑中应用此行为。

关于邮件服务器交互的另一个有趣的方面是,Citrus 邮件服务器组件还能够模拟邮件服务器错误。

模拟邮件服务器错误
t.run(receive()
    .endpoint(mailServer)
    .message(MailMessage.request("foodmarket@quarkus.io", "${client}@quarkus.io", "Booking completed!")
            .body("Hey ${client}, your booking ${product} has been completed.", "text/plain"))
);

t.run(send()
    .endpoint(mailServer)
    .message(MailMessage.response(443, "Failed!"))
);

这次 Citrus 邮件服务器明确响应 443 Failed! 错误,而 Quarkus 应用需要相应地处理此错误。在自动化集成测试中验证错误场景非常重要,有助于我们开发健壮的应用。能够通过 Citrus 在自动化测试中触发这些错误场景是很好的。

总结

在本文中,我们了解了如何将 Citrus 测试框架与 Quarkus 测试 dev services 相结合,以执行面向事件驱动的应用程序的自动化集成测试。该测试能够生成/消费 Kafka 流上的事件,并通过验证 Json 数据和数据库中持久化的实体来相应地验证 Quarkus 应用。

Citrus 作为一个框架提供了许多模块,每个模块都提供端点(客户端和服务器)以在集成测试期间进行直接的消息传递交互(例如,Kafka、JMS、FTP、Http、SOAP、Mail 等)。消息验证功能允许我们使用不同的格式(例如,Json、XML、纯文本)验证交换的消息内容。

虽然 Citrus 项目已经存在相当长一段时间了,但 Citrus Quarkus 扩展是 Citrus 最新版本 4.0 中的一个新功能。一如既往,非常感谢您的反馈!请尝试一下,让我们知道您对这种结合 Citrus 和 Quarkus 测试的自动化集成测试方法的看法。