Core Java

Quarkus Citrus Test Tutorial

Quarkus, as a Kubernetes-native Java stack, provides an efficient environment for building Java applications, especially for containerized environments. Testing is critical to ensure these applications work as intended. Let us delve into understanding the Quarkus Citrus test.

1. Introduction and the Purpose of Citrus

Citrus is an open-source integration testing framework designed for testing complex messaging systems and microservices. It enables developers to simulate and validate message exchanges between different systems, using a variety of protocols like HTTP, REST, SOAP, JMS, and more. Citrus excels in scenarios where different systems need to communicate with one another, allowing testers to mock external services, send requests, and verify responses without needing the actual services to be available during testing. The framework supports both client and server-side testing, making it ideal for testing microservices, enterprise applications, and distributed systems. It is highly customizable, allowing the creation of reusable test scenarios that can be easily integrated into a continuous integration (CI) pipeline. With its wide range of protocol support and ability to simulate realistic conditions, Citrus is a powerful tool for ensuring system reliability and performance in production environments.

Citrus is an integration testing framework that simulates communication between components in distributed systems. It is ideal for testing message-based interfaces like HTTP, REST, JMS, and database access. By simulating requests and responses, Citrus helps ensure that your services behave correctly under different scenarios, particularly useful in microservices environments.

1.1 Advantages

  • Broad Protocol Support: Citrus supports a wide range of messaging protocols such as HTTP, REST, JMS, and SOAP, making it highly flexible for integration testing.
  • Test Reusability: Citrus allows you to create reusable test scenarios that can be applied across different components of the application.
  • Simulation of External Services: Citrus can easily mock external services, allowing you to simulate different conditions for testing.

1.2 Disadvantages

  • Learning Curve: Citrus has a steep learning curve, particularly for developers unfamiliar with message-based testing frameworks.
  • Performance Impact: In certain cases, the setup for each test can be time-consuming, especially when testing high-concurrency environments.
  • Limited Quarkus Integration: While Citrus can be integrated with Quarkus, there are limitations, such as lacking native support for Quarkus-specific features like reactive programming.

1.3 Challenges

Testing Quarkus applications with Citrus presents several challenges:

  • Configuration Overhead: Citrus requires configuring various components like HTTP clients, data sources, and JMS connections, which can be cumbersome in complex applications.
  • Integration Complexity: Although Citrus works well with Quarkus, some features like dependency injection (CDI) might need extra configuration to fully integrate Citrus with Quarkus’s ecosystem.
  • Test Performance: When testing large-scale messaging or database operations, test execution might become slower due to the setup and teardown of resources.

2. Citrus Tests With Quarkus

Citrus can be used to test Quarkus applications by simulating requests to the REST API, database, or messaging services. Below are examples of how to test REST endpoints, database access, and messaging with Citrus.

2.1 Setting Up Dependencies

Add the following dependencies to your pom.xml:

<dependencies>
	<!-- Quarkus test dependencies -->
	<dependency>
		<groupId>io.quarkus</groupId>
		<artifactId>quarkus-junit5</artifactId>
		<scope>test</scope>
	</dependency>
	<!-- Citrus dependencies -->
	<dependency>
		<groupId>com.consol.citrus</groupId>
		<artifactId>citrus-junit5</artifactId>
		<version>3.1.0</version>
		<scope>test</scope>
	</dependency>
	<!-- Citrus database support -->
	<dependency>
		<groupId>com.consol.citrus</groupId>
		<artifactId>citrus-jdbc</artifactId>
		<version>3.1.0</version>
	</dependency>
	<!-- Citrus JMS support -->
	<dependency>
		<groupId>com.consol.citrus</groupId>
		<artifactId>citrus-jms</artifactId>
		<version>3.1.0</version>
	</dependency>
</dependencies>

2.2 Testing a REST Endpoint

In this example, we’ll test a REST endpoing and validate the result.

import com.consol.citrus.dsl.junit5.CitrusSupport;
import org.junit.jupiter.api.Test;
import static com.consol.citrus.http.actions.HttpActionBuilder.http;

@CitrusSupport
public class HelloEndpointTest {

  @Test
  public void testHelloEndpoint() {
    runner().http(httpActionBuilder -> httpActionBuilder
      .client("httpClient")
      .send()
      .get("/api/hello"));

    runner().http(httpActionBuilder -> httpActionBuilder
      .client("httpClient")
      .receive()
      .response(200)
      .message()
      .body("{\"message\":\"Hello, World!\"}"));
  }
}

2.2.1 Explanation

The code defines a:

  • @CitrusSupport: This annotation enables Citrus support in the test class, allowing the use of Citrus testing utilities and methods within JUnit 5.
  • public class HelloEndpointTest: Defines a JUnit 5 test class named HelloEndpointTest which will contain the test logic for the Quarkus REST endpoint.
  • @Test: Marks the method testHelloEndpoint() as a test case that will be executed by the JUnit framework.
  • runner().http(httpActionBuilder -> httpActionBuilder.client("httpClient").send().get("/api/hello")):
    • Creates a new HTTP action using Citrus’s runner() method.
    • client("httpClient"): Specifies the HTTP client to be used for sending the request. In this case, "httpClient" is a pre-configured HTTP client defined in the Citrus test configuration.
    • send(): Initiates an HTTP request.
    • get("/api/hello"): Sends an HTTP GET request to the URL /api/hello, targeting the REST endpoint of the Quarkus application.
  • runner().http(httpActionBuilder -> httpActionBuilder.client("httpClient").receive().response(200).message().body("{\"message\":\"Hello, World!\"}")):
    • Creates another HTTP action to receive the response.
    • client("httpClient"): Uses the same HTTP client that sent the request to also receive the response.
    • receive(): Indicates that the client will now receive a response.
    • response(200): Specifies that the expected HTTP response status code is 200 (OK).
    • message(): Initiates the validation of the response message.
    • body("{\"message\":\"Hello, World!\"}"): Asserts that the body of the response contains the JSON message {"message":"Hello, World!"}.

2.2.2 Output

If the response from the /api/hello endpoint matches the expected result (status 200 and body {“message”:”Hello, World!”}), the test will pass, and no errors will be reported.

Test Passed: The HTTP GET request to /api/hello returned status 200 with the expected body {"message":"Hello, World!"}.

If the status code or the response body doesn’t match the expected values, the test will fail, and an error message will be displayed, indicating the mismatch.

Test Failed: Expected HTTP status 200 but received 404.
Test Failed: Expected response body {"message":"Hello, World!"} but received {"error":"Not Found"}.

2.3 Testing Database Access

Citrus can also simulate database operations to test persistence layers. In this example, we’ll test a database query execution and validate the result.

import com.consol.citrus.dsl.junit5.CitrusSupport;
import org.junit.jupiter.api.Test;
import static com.consol.citrus.actions.ExecuteSQLAction.Builder.sql;

@CitrusSupport
public class DatabaseAccessTest {

  @Test
  public void testDatabaseQuery() {
    runner().sql(sqlActionBuilder -> sqlActionBuilder
      .dataSource("myDataSource")
      .statement("SELECT * FROM users WHERE id = 1"));

    runner().sql(sqlActionBuilder -> sqlActionBuilder
      .validate("id", "1")
      .validate("name", "John Doe"));
  }
}

2.3.1 Explanation

The code defines a:

  • @CitrusSupport: This annotation enables Citrus support in the test class, making Citrus’s functionality available within JUnit 5.
  • public class DatabaseAccessTest: Defines a JUnit 5 test class named DatabaseAccessTest that will contain the logic for testing database operations.
  • @Test: Marks the method testDatabaseQuery() as a test case to be executed by the JUnit framework.
  • runner().sql(sqlActionBuilder -> sqlActionBuilder.dataSource("myDataSource").statement("SELECT * FROM users WHERE id = 1")):
    • Creates a new SQL action using Citrus’s runner() method to execute SQL statements.
    • dataSource("myDataSource"): Specifies the database connection (data source) to be used for the query. In this case, "myDataSource" is a configured data source in the test environment.
    • statement("SELECT * FROM users WHERE id = 1"): Executes the SQL query SELECT * FROM users WHERE id = 1, fetching user data from the database with ID 1.
  • runner().sql(sqlActionBuilder -> sqlActionBuilder.validate("id", "1").validate("name", "John Doe")):
    • Creates another SQL action to validate the results of the previously executed query.
    • validate("id", "1"): Validates that the value for the column id in the result set is 1.
    • validate("name", "John Doe"): Validates that the value for the column name in the result set is John Doe.

2.3.2 Output

The test executes a SQL query to retrieve a user with an id of 1 from the database using the data source myDataSource. The following is the output based on the query and validation:

2.3.2.1 Test Output (If Successful)

The SQL statement SELECT * FROM users WHERE id = 1 is executed successfully. The query returns a result set where:

  • id = 1
  • name = John Doe

Both validations for id and name pass.

2.3.2.2 Test Output (If Failed)
  • If the result set does not contain an entry with id = 1: Expected id = 1 but found no matching entry.
  • If the result set returns an unexpected name: Expected name = John Doe but found [Actual Name].

2.4 Testing Messaging (JMS)

Citrus supports JMS messaging, enabling us to test message producers and consumers. In this example, we’ll send a message to a JMS queue and verify its content.

import com.consol.citrus.dsl.junit5.CitrusSupport;
import org.junit.jupiter.api.Test;
import static com.consol.citrus.jms.actions.JmsActionBuilder.jms;

@CitrusSupport
public class MessagingTest {

  @Test
  public void testSendMessage() {
    runner().jms(jmsActionBuilder -> jmsActionBuilder
      .send()
      .queue("testQueue")
      .message()
      .body("<message>Hello, JMS!</message>"));

    runner().jms(jmsActionBuilder -> jmsActionBuilder
      .receive()
      .queue("testQueue")
      .message()
      .body("<message>Hello, JMS!</message>"));
  }
}

2.4.1 Explanation

The code defines a:

  • @CitrusSupport: This annotation enables Citrus support in the test class, allowing the use of Citrus testing functionality within a JUnit 5 test environment.
  • public class MessagingTest: Defines a JUnit 5 test class named MessagingTest, which contains logic for testing JMS (Java Messaging Service) message sending and receiving.
  • @Test: Marks the method testSendMessage() as a test case to be executed by the JUnit framework.
  • runner().jms(jmsActionBuilder -> jmsActionBuilder.send().queue("testQueue").message().body("<message>Hello, JMS!</message>")):
    • Creates a new JMS action using Citrus’s runner() method to send a message.
    • send(): Specifies that this action will send a message to a JMS queue.
    • queue("testQueue"): Specifies the target queue where the message will be sent. In this case, the queue is named "testQueue".
    • message(): Begins the construction of the message to be sent.
    • body("<message>Hello, JMS!</message>"): Defines the content of the message. In this case, the message body is an XML snippet that says Hello, JMS!.
  • runner().jms(jmsActionBuilder -> jmsActionBuilder.receive().queue("testQueue").message().body("<message>Hello, JMS!</message>")):
    • Creates another JMS action using Citrus to receive a message from the queue.
    • receive(): Specifies that this action will receive a message from the JMS queue.
    • queue("testQueue"): Specifies the queue from which the message will be received. In this case, it is the same "testQueue" to which the message was sent.
    • message(): Begins the construction of the expected message to be received.
    • body("<message>Hello, JMS!</message>"): Validates that the message body received from the queue matches the expected content <message>Hello, JMS!</message>.

2.4.2 Output

The test simulates sending and receiving a JMS message to and from the testQueue. The following describes the expected output based on the messaging operations.

2.4.2.1 Test Output (If Successful)
  • A JMS message with the body <message>Hello, JMS!</message> is successfully sent to the queue testQueue.
  • The message is received from the same testQueue, and the message body is validated to match <message>Hello, JMS!</message>. The message tag signifies queue symbolism.
  • Both the send and receive operations are complete without errors, and the message content is validated successfully.
2.4.2.2 Test Output (If Failed)
  • If the message is not found in the queue:
    • Error: Expected to receive a message from testQueue, but no message was found.
  • If the received message body does not match the expected value:
    • Error: Expected message body <message>Hello, JMS!</message> but received [Actual Message].

3. Conclusion

Testing Quarkus applications with Citrus offers significant advantages, especially when dealing with complex messaging and database scenarios. While there are some challenges in terms of configuration and performance, the ability to simulate real-world interactions with external services and databases makes Citrus a valuable tool. By leveraging Citrus, developers can ensure their Quarkus applications are robust and ready for production environments.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button