1. 概述

Quarkus(超音速亚原子Java)承诺提供小型构件、极快启动时间和更低的首请求延迟。我们可以将其理解为整合了Java标准技术(Jakarta EE、MicroProfile等)的框架,能构建可部署到任何容器运行时的独立应用,轻松满足云原生应用需求。

本文将学习如何使用Citrus框架实现集成测试。Citrus由Red Hat首席软件工程师Christoph Deppisch开发。

2. Citrus的核心价值

我们开发的应用通常不会孤立运行,而是与数据库、消息系统或在线服务等外部系统交互。测试时,虽然可以通过模拟对象进行隔离测试,但有时需要验证应用与外部系统的通信能力,这正是Citrus的用武之地

下面分析常见交互场景:

2.1. HTTP交互

Web应用可能提供基于HTTP的API(如REST API)。Citrus可作为HTTP客户端调用应用API并验证响应(类似REST-assured)。当应用作为其他服务的消费者时,Citrus能启动嵌入式HTTP服务器模拟外部服务:

quarkus citrus 01 http

2.2. Kafka交互

当应用作为Kafka消费者时,Citrus可充当生产者向主题发送记录,触发应用消费处理。若应用是生产者,Citrus则作为消费者验证发送到主题的消息:

quarkus citrus 02 kafka

2.3. 关系型数据库交互

应用使用关系型数据库时,Citrus能作为JDBC客户端验证数据库状态。此外,它还提供JDBC驱动和嵌入式数据库模拟,可配置返回特定测试结果并验证执行的查询:

quarkus citrus 03 jdbc

2.4. 其他支持

Citrus还支持REST、SOAP、JMS、WebSocket、邮件、FTP和Apache Camel接口等系统,完整列表见官方文档

3. 在Quarkus中使用Citrus测试

Quarkus对集成测试有完善支持,包括模拟、测试配置文件和原生可执行文件测试。Citrus提供QuarkusTest运行时——一种Quarkus测试资源,用于扩展Quarkus测试能力。

以典型场景为例:REST服务提供者将数据存入关系型数据库,并在创建新条目时向Kafka发送消息。对Citrus而言,具体实现细节无关紧要,应用被视为黑盒,只需关注外部系统和通信通道:

quarkus citrus 10 appsample

3.1. Maven依赖

在Quarkus项目中使用Citrus,需添加*citrus-bom*:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.citrusframework</groupId>
            <artifactId>citrus-bom</artifactId>
            <version>4.2.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.citrusframework</groupId>
        <artifactId>citrus-quarkus</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

根据技术栈选择性添加模块:

<dependency>
    <groupId>org.citrusframework</groupId>
    <artifactId>citrus-openapi</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.citrusframework</groupId>
    <artifactId>citrus-http</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.citrusframework</groupId>
    <artifactId>citrus-validation-json</artifactId>
</dependency>
<dependency>
    <groupId>org.citrusframework</groupId>
    <artifactId>citrus-validation-hamcrest</artifactId>
    <version>${citrus.version}</version>
</dependency>
<dependency>
    <groupId>org.citrusframework</groupId>
    <artifactId>citrus-sql</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.citrusframework</groupId>
    <artifactId>citrus-kafka</artifactId>
    <scope>test</scope>
</dependency>

3.2. 应用配置

Citrus无需全局Quarkus配置。只需在application.properties中添加以下配置避免日志警告:

%test.quarkus.arc.ignored-split-packages=org.citrusframework.*

3.3. 边界测试设置

典型Citrus测试包含以下元素:

  • @CitrusSupport注解:添加Quarkus测试资源扩展测试能力
  • @CitrusConfiguration注解:包含Citrus配置类,用于全局配置接口和依赖注入
  • 注入字段:获取Citrus提供的接口和其他对象

测试边界时需要HTTP客户端向应用发送请求并验证响应。首先创建Citrus配置类:

public class BoundaryCitrusConfig {

    public static final String API_CLIENT = "apiClient";

    @BindToRegistry(name = API_CLIENT)
    public HttpClient apiClient() {
        return http()
          .client()
          .requestUrl("http://localhost:8081")
          .build();
    }

}

然后创建测试类:

⚠️ 按约定,若声明方法与测试类字段同名可省略name属性。虽更简洁但缺少编译检查,易出错。

3.4. 边界测试实现

编写测试需了解Citrus的声明式概念,包含以下组件:

quarkus citrus 11 concept

  • **测试上下文(Test Context)**:提供测试变量和函数,用于替换消息负载和头信息中的动态内容
  • **测试动作(Test Action)**:抽象测试步骤(如发送请求/接收响应)或简单操作(输出/计时)
  • **测试动作构建器(Test Action Builder)**:使用构建器模式定义测试动作
  • **测试动作运行器(Test Action Runner)**:执行测试动作并提供上下文,BDD风格可使用GherkinTestActionRunner

可注入测试动作运行器。以下代码向http://localhost:8081/api/v1/todos发送POST请求并验证201状态码:

@CitrusResource
GherkinTestActionRunner t;

@Test
void shouldReturn201OnCreateItem() {
    t.when(
            http()
              .client(apiClient)
              .send()
              .post("/api/v1/todos")
              .message()
              .contentType(MediaType.APPLICATION_JSON)
              .body("{\"title\": \"test\"}")
    );
    t.then(
            http()
              .client(apiClient)
              .receive()
              .response(HttpStatus.CREATED)
    );
}

✅ 直接使用JSON字符串定义请求体,也可参考示例使用数据字典。

消息验证支持多种方式(见文档)。例如结合JSON-Path和Hamcrest扩展验证:

t.then(
        http()
          .client(apiClient)
          .receive()
          .response(HttpStatus.CREATED)
          .message()
          .type(MessageType.JSON)
          .validate(
                    jsonPath()
                      .expression("$.title", "test")
                      .expression("$.id", is(notNullValue()))
          )
);

❌ 目前仅支持Hamcrest,AssertJ支持自2016年悬而未决

3.5. 基于OpenAPI的边界测试

可基于OpenAPI定义发送请求,自动验证响应是否符合Schema中声明的属性和头信息约束

首先加载OpenAPI Schema。若项目中有YML文件:

final OpenApiSpecification apiSpecification = OpenApiSpecification.from(
        Resources.create("classpath:openapi.yml")
);

或从运行中的Quarkus应用读取:

final OpenApiSpecification apiSpecification = OpenApiSpecification.from(
    "http://localhost:8081/q/openapi"
);

测试中通过operationId引用操作:

t.when(
        openapi()
          .specification(apiSpecification)
          .client(apiClient)
          .send("createTodo") // operationId
);
t.then(
        openapi()
          .specification(apiSpecification)
          .client(apiClient)
          .receive("createTodo", HttpStatus.CREATED)
);

⚠️ 当前无法显式定义头信息/参数/请求体值(见GitHub Issue),生成随机日期值存在bug。可通过跳过可选字段规避:

@BeforeEach
void setup() {
    this.apiSpecification.setGenerateOptionalFields(false);
    this.apiSpecification.setValidateOptionalFields(false);
}

此时需禁用严格验证,否则因服务返回可选字段会失败(见Issue)。使用JUnit Pioneer实现:

<dependency>
    <groupId>org.junit-pioneer</groupId>
    <artifactId>junit-pioneer</artifactId>
    <version>2.2.0</version>
    <scope>test</scope>
</dependency>

在测试类@CitrusSupport前添加:

@SetSystemProperty(
    key = "citrus.json.message.validation.strict",
    value = "false"
)

3.6. 数据库访问测试

调用REST创建接口后,应将新条目存入数据库。可通过查询新创建的ID验证数据持久化

首先注入Quarkus数据源:

@Inject
DataSource dataSource;

从响应体提取ID并存储为测试上下文变量:

t.when(
        http()
          .client(apiClient)
          .send()
          .post("/api/v1/todos")
          .message()
          .contentType(MediaType.APPLICATION_JSON)
          .body("{\"title\": \"test\"}")
);
t.then(
        http()
          .client(apiClient)
          .receive()
          .response(HttpStatus.CREATED)
          // 将新ID保存到测试变量"todoId"
          .extract(fromBody().expression("$.id", "todoId"))
);

使用变量查询数据库:

t.then(
        sql()
          .dataSource(dataSource)
          .query()
          .statement("select title from todos where id=${todoId}")
          .validate("title", "test")
);

3.7. 消息传递测试

调用REST创建接口后,应向Kafka主题发送新条目消息。可通过订阅主题消费并验证消息

定义Citrus接口:

public class KafkaCitrusConfig {

    public static final String TODOS_EVENTS_TOPIC = "todosEvents";

    @BindToRegistry(name = TODOS_EVENTS_TOPIC)
    public KafkaEndpoint todosEvents() {
        return kafka()
          .asynchronous()
          .topic("todo-events")
          .build();
    }

}

在测试类中注入接口:

@QuarkusTest
@CitrusSupport
@CitrusConfiguration(classes = {
    BoundaryCitrusConfig.class,
    KafkaCitrusConfig.class
})
class MessagingCitrusTest {

    @CitrusEndpoint(name = KafkaCitrusConfig.TODOS_EVENTS_TOPIC)
    KafkaEndpoint todosEvents;

    // ...

}

发送请求后订阅主题验证消息:

t.and(
        receive()
          .endpoint(todosEvents)
          .message()
          .type(MessageType.JSON)
          .validate(
                    jsonPath()
                      .expression("$.title", "test")
                      .expression("$.id", "${todoId}")
          )
);

3.8. 模拟外部服务

Citrus可模拟外部系统,避免测试依赖真实环境,直接验证发送消息并模拟响应。

以Kafka为例,Quarkus Dev Services会启动Kafka容器,可用Citrus模拟替代。在application.properties中禁用Dev Services:

%test.quarkus.kafka.devservices.enabled=false

配置Citrus模拟服务器:

public class EmbeddedKafkaCitrusConfig {

    private EmbeddedKafkaServer kafkaServer;

    @BindToRegistry
    public EmbeddedKafkaServer kafka() {
        if (null == kafkaServer) {
            kafkaServer = new EmbeddedKafkaServerBuilder()
              .kafkaServerPort(9092)
              .topics("todo-events")
              .build();
        }
        return kafkaServer;
    }

    // 测试后停止服务器
    @BindToRegistry
    public AfterSuite afterSuiteActions() {
        return afterSuite()
          .actions(context -> kafka().stop())
          .build();
    }

}

在测试类中激活模拟:

@QuarkusTest
@CitrusSupport
@CitrusConfiguration(classes = {
    BoundaryCitrusConfig.class,
    KafkaCitrusConfig.class,
    EmbeddedKafkaCitrusConfig.class
})
class MessagingCitrusTest {

    // ...

}

还支持模拟HTTP服务关系型数据库

4. 挑战与注意事项

使用Citrus测试存在以下挑战:

  • API不够直观
  • 缺少AssertJ集成
  • 验证失败时抛出异常而非AssertionError,导致测试报告混乱
  • 在线文档虽全面但代码示例多为Groovy/XML
  • Java代码示例可参考GitHub仓库
  • Javadoc文档不完整

⚠️ 框架似乎更侧重Spring集成,文档常引用Spring配置。citrus-jdbc模块依赖Spring Core/JDBC,需显式排除以避免引入不必要的传递依赖。

5. 总结

本文介绍了如何使用Citrus实现Quarkus集成测试。Citrus提供了丰富功能测试应用与外部系统的通信,包括模拟外部系统。虽然文档完善,但代码示例与Quarkus集成场景匹配度不高。幸运的是,官方提供了Quarkus示例仓库

所有代码实现可在GitHub获取。


原始标题:Testing Quarkus With Citrus | Baeldung