使用 Citrus 測試 Quarkus
1. 概述
Quarkus,超音速亞原子 Java,承諾提供小工件、極快的啟動時間和更低的首次請求時間。我們可以將其理解為一個框架,它整合了Java標準技術(Jakarta EE、MicroProfile等),能夠建立可部署在任何容器運行時的獨立應用程序,輕鬆滿足雲端原生應用程式的需求。
在本文中,我們將學習如何使用Citrus實施整合測試,Citrus 是由 Red Hat 首席軟體工程師Christoph Deppisch編寫的框架。
2. 柑橘的用途
我們開發的應用程式通常不會獨立運行,而是與其他系統通信,例如資料庫、訊息傳遞系統或線上服務。在測試我們的應用程式時,我們可以透過模擬相應的物件以隔離的方式完成此操作。但我們也可能想測試我們的應用程式與外部系統的通訊。這就是柑橘發揮作用的地方。
讓我們仔細看看最常見的互動場景。
2.1. HTTP協定
我們的 Web 應用程式可能具有基於 HTTP 的 API(例如 REST API)。 Citrus 可以充當 HTTP 用戶端,呼叫我們應用程式的 HTTP API 並驗證回應(就像 REST-assured 所做的那樣)。我們的應用程式也可能是另一個應用程式的 HTTP API 的使用者。 Citrus 可以運行嵌入式 HTTP 伺服器並在這種情況下充當模擬:
2.2.卡夫卡
在本例中,我們的應用程式是 Kafka 消費者。 Citrus 可以充當 Kafka 生產者,將記錄傳送到主題,以便我們的應用程式透過消費該記錄來觸發。我們的應用程式也可能是 Kafka 生產者。
Citrus 可以充當消費者來驗證我們的應用程式在測試期間發送到主題的訊息。此外,Citrus 提供了一個嵌入式 Kafka 伺服器,以便在測試過程中獨立於任何外部伺服器:
2.3.關聯式資料庫
我們的應用程式使用關聯式資料庫。 Citrus 可以充當 JDBC 用戶端,驗證資料庫是否具有預期狀態。此外,Citrus 還提供了 JDBC 驅動程式和嵌入式資料庫模擬,可用於傳回特定於測試案例的結果並驗證執行的資料庫查詢:
2.4.進一步支持
Citrus 支援更多外部系統,例如 REST、SOAP、JMS、Websocket、Mail、FTP 和 Apache Camel 端點。我們可以在文件中找到完整的清單。
3. 使用 Quarkus 進行柑橘測試
Quarkus 對編寫整合測試提供廣泛支持,包括模擬、測試設定檔和測試本機可執行檔。 Citrus 提供QuarkusTest 運行時,這是一個Quarkus 測試資源,透過包含 Citrus 功能來擴展基於 Quarkus 的測試。
讓我們來看看一個使用最常見技術的範例 - REST 服務提供程序,它將資料儲存在關聯式資料庫中,並在建立新專案時向 Kafka 發送訊息。對於 Citrus 來說,我們如何詳細實現這一點並不重要。我們的應用程式是一個黑盒子,只有外部系統和通訊管道才是至關重要的:
3.1. Maven 依賴項
要在基於 Quarkus 的專案中使用 Citrus,我們可以使用[citrus-bom](https://mvnrepository.com/artifact/org.citrusframework/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 測試資源以擴展基於 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();
}
}
然後,我們建立測試類別:
@QuarkusTest
@CitrusSupport
@CitrusConfiguration(classes = {
BoundaryCitrusConfig.class
})
class CitrusTests {
@CitrusEndpoint(name = BoundaryCitrusConfig.API_CLIENT)
HttpClient apiClient;
}
作為約定,如果宣告方法和測試類別中的欄位具有相同的名稱,我們可以跳過註解的名稱屬性。這可能會更短,但由於缺少編譯器檢查而容易出現錯誤。
3.4.測試邊界
為了編寫測試,我們需要知道 Citrus 有一個聲明性概念,定義元件:
-
Test Context
是一個提供測試變數和函數的對象,其中包括替換訊息有效負載和標頭中的動態內容。 -
Test Action
是測試中每個步驟的抽象。這可能是一種交互,例如發送請求或接收回應,包括驗證和驗證。它也可以只是一個簡單的輸出或計時器。 Citrus 提供了 Java DSL 和XML作為使用測試操作定義測試定義的替代方案。我們可以在文件中找到預定義測試操作的清單。 -
Test Action Builder
用於定義和建置測試操作。 Citrus 在這裡使用建構器模式。 -
Test Action Runner
使用測試操作產生器來建立測試操作。然後,它執行測試操作,提供測試上下文。對於 BBD 風格,我們可以使用 GherkinTestActionRunner。
我們也可以注入 Test Action Runner。程式碼顯示了一個測試,該測試使用 JSON 正文向http://localhost:8081/api/v1/todos
發送 HTTP 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,我們可以擴充then
區塊:
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 年在GitHub 上提出了一個問題。
3.5.基於OpenAPI測試邊界
我們也可以根據 OpenAPI 定義發送請求。這會自動驗證有關 OpenAPI 模式中聲明的屬性和標頭約束的回應。
首先,我們需要載入 OpenAPI 架構。例如,如果我們的專案中有一個 YML 文件,我們可以透過定義一個OpenApiSpecification
欄位來做到這一點:
final OpenApiSpecification apiSpecification = OpenApiSpecification.from(
Resources.create("classpath:openapi.yml")
);
我們也可以從正在運行的 Quarkus 應用程式中讀取 OpenApi(如果可用):
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 )。此外,產生隨機日期值時存在錯誤。我們至少可以透過跳過隨機值來避免可選欄位的這種情況:
@BeforeEach
void setup() {
this.apiSpecification.setGenerateOptionalFields(false);
this.apiSpecification.setValidateOptionalFields(false);
}
在這種情況下,我們還必須停用嚴格驗證,這將失敗,因為服務會傳回可選欄位。 (請參閱此GitHub-Issue )我們可以使用 JUnit Pioneer 來做到這一點。為此,我們加入junit-pioneer依賴項:
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
<version>2.2.0</version>
<scope>test</scope>
</dependency>
然後,我們可以在@CitrusSupport
註解之前將@SystemProperty
註解加入測試類別:
@SetSystemProperty(
key = "citrus.json.message.validation.strict",
value = "false"
)
3.6.測試資料庫訪問
當我們呼叫 REST API 的建立操作時,它應該將新專案儲存在資料庫中。為了評估這一點,我們可以在資料庫中查詢新建立的 ID。
首先,我們需要一個資料來源。我們可以從 Quarkus 輕鬆注入:
@Inject
DataSource dataSource;
然後,我們需要從回應正文中提取新建立的項目的 ID 並將其儲存為Test Context
變數:
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)
// save new id to test context variable "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 API 的建立操作時,它應該會將新專案傳送到 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();
}
}
然後,我們希望 Citrus 將此端點注入到我們的測試中:
@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 伺服器的 Docker 容器。我們可以使用 Citrus 模擬來代替。然後,我們必須在application.properties
檔案中停用開發服務功能:
%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;
}
// stop the server after the test
@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 的整合。當驗證失敗時,Citrus 會拋出異常而不是AssertionError
,導致測試報告混亂。線上文件很豐富,但程式碼範例包含 Groovy 程式碼,有時還包含 XML。 GitHub 中有一個包含 Java 程式碼範例的儲存庫,可能會有所幫助。 Javadoc 不完整。
看起來重點是整合到 Spring 框架中。該文件通常引用 Spring 中的 Citrus 配置。 citrus-jdbc
模組依賴 Spring Core 和 Spring JDBC,除非我們排除它們,否則我們將在測試中將它們作為不必要的傳遞依賴項。
5. 結論
在本教程中,我們學習如何使用 Citrus 實作 Quarkus 測試。 Citrus 提供了許多功能來測試我們的應用程式與外部系統的通訊。這也包括模擬這些系統進行測試。它有詳細的文檔記錄,但所包含的程式碼範例與整合到 Quarkus 之外的其他用例相符。幸運的是,有一個GitHub 儲存庫,其中包含 Quarkus 的範例。
與往常一樣,所有程式碼實作都可以在 GitHub 上取得。