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服务器模拟外部服务:
2.2. Kafka交互
当应用作为Kafka消费者时,Citrus可充当生产者向主题发送记录,触发应用消费处理。若应用是生产者,Citrus则作为消费者验证发送到主题的消息:
2.3. 关系型数据库交互
应用使用关系型数据库时,Citrus能作为JDBC客户端验证数据库状态。此外,它还提供JDBC驱动和嵌入式数据库模拟,可配置返回特定测试结果并验证执行的查询:
2.4. 其他支持
Citrus还支持REST、SOAP、JMS、WebSocket、邮件、FTP和Apache Camel接口等系统,完整列表见官方文档。
3. 在Quarkus中使用Citrus测试
Quarkus对集成测试有完善支持,包括模拟、测试配置文件和原生可执行文件测试。Citrus提供QuarkusTest运行时——一种Quarkus测试资源,用于扩展Quarkus测试能力。
以典型场景为例:REST服务提供者将数据存入关系型数据库,并在创建新条目时向Kafka发送消息。对Citrus而言,具体实现细节无关紧要,应用被视为黑盒,只需关注外部系统和通信通道:
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的声明式概念,包含以下组件:
- **测试上下文(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 {
// ...
}
4. 挑战与注意事项
使用Citrus测试存在以下挑战:
- API不够直观
- 缺少AssertJ集成
- 验证失败时抛出异常而非
AssertionError
,导致测试报告混乱 - 在线文档虽全面但代码示例多为Groovy/XML
- Java代码示例可参考GitHub仓库
- Javadoc文档不完整
⚠️ 框架似乎更侧重Spring集成,文档常引用Spring配置。
citrus-jdbc
模块依赖Spring Core/JDBC,需显式排除以避免引入不必要的传递依赖。
5. 总结
本文介绍了如何使用Citrus实现Quarkus集成测试。Citrus提供了丰富功能测试应用与外部系统的通信,包括模拟外部系统。虽然文档完善,但代码示例与Quarkus集成场景匹配度不高。幸运的是,官方提供了Quarkus示例仓库。
所有代码实现可在GitHub获取。