Apache Camel is a fantastic Open Source integration framework that empowers you to quickly and easily integrate various systems consuming or producing data.
Camel implements the enterprise integration patterns for building mediation and routing rules in your software. With the Camel support in Citrus you are able to directly interact with the 300+ Camel components through route definitions. You can call Camel routes and receive synchronous response messages from a Citrus test. You can also simulate the Camel route endpoint with receiving messages and providing simulated response messages.
|
Note
|
The camel components in Citrus are located in a separate Maven module. So you should add the module as Maven dependency to your project accordingly. |
<dependency>
<groupId>org.citrusframework</groupId>
<artifactId>citrus-camel</artifactId>
<version>${citrus.version}</version>
</dependency>For XML configurations Citrus provides a special Camel configuration schema that is used in Spring configuration files.
You have to include the citrus-camel namespace in your Spring configuration XML files as follows.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:citrus="http://www.citrusframework.org/schema/config"
xmlns:citrus-camel="http://www.citrusframework.org/schema/camel/config"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.citrusframework.org/schema/config
http://www.citrusframework.org/schema/config/citrus-config.xsd
http://www.citrusframework.org/schema/camel/config
http://www.citrusframework.org/schema/camel/config/citrus-camel-config.xsd">
[...]
</beans>Now you are ready to use the Camel configuration elements using the citrus-camel namespace prefix.
The next sections explain the Citrus capabilities while working with Apache Camel.
Camel and Citrus both use the endpoint pattern in order to define message destinations. Users can interact with these endpoints when creating the mediation and routing logic. The Citrus endpoint component for Camel interaction is defined as follows in your Citrus Spring configuration.
@Bean
public CamelEndpoint helloCamelEndpoint() {
return new CamelEndpointBuilder()
.endpointUri("direct:hello")
.build();
}<citrus-camel:endpoint id="helloCamelEndpoint"
endpoint-uri="direct:hello"/>The endpoint defines an endpoint uri that will be called when messages are sent/received using the endpoint producer or consumer.
The endpoint uri refers to a Camel route that is located inside a Camel context.
The Camel route should use the respective endpoint uri as from definition.
@Bean
public CamelContext camelContext() {
CamelContext context = new DefaultCamelContext();
context.addRoutes(new RouteBuilder() {
@Override
public void configure() throws Exception {
from("direct:hello")
.routeId("helloRoute")
.to("log:org.citrusframework.camel?level=INFO")
.to("seda:greetings");
}
});
return context;
}<camelContext id="camelContext" xmlns="http://camel.apache.org/schema/spring">
<route id="helloRoute">
<from uri="direct:hello"/>
<to uri="log:org.citrusframework.camel?level=INFO"/>
<to uri="seda:greetings-feed"/>
</route>
</camelContext>In the example above the Camel context is placed as Spring bean. The context defines a Camel route the Citrus test case can interact with.
The Camel context is automatically referenced in the Citrus Camel endpoint. This is because Citrus will automatically look for a Camel context in the Spring bean configuration.
In case you have multiple Camel context instances in your configuration you can explicitly link the endpoint to a
context with camel-context="camelContext".
@Bean
public CamelEndpoint helloCamelEndpoint() {
return new CamelEndpointBuilder()
.camelContext(specialCamelContext)
.endpointUri("direct:hello")
.build();
}<citrus-camel:endpoint id="helloCamelEndpoint"
camel-contxt="specialCamelContext"
endpoint-uri="direct:hello"/>This explicitly binds the endpoint to the context named "specialCamelContext". This configuration would be the easiest setup to use Camel with Citrus as you can add the Camel context straight to the Spring bean application context and interact with it in Citrus. Of course, you can also import your Camel context and routes from other Spring bean context files, or you can start the Camel context routes with Java code.
In the example the Camel route is listening on the route endpoint uri direct:hello.
Incoming messages will be logged to the console using a log Camel component.
After that the message is forwarded to a seda Camel component which is a simple queue in memory.
The Citrus endpoint can interact with this sample route definition sending messages to the direct:hello endpoint.
The endpoint configuration holds the endpoint uri information that tells Citrus how to access the Camel route.
This endpoint uri can be any Camel endpoint uri that is used in a Camel route.
The Camel routes support asynchronous and synchronous message communication patterns. By default, Citrus uses asynchronous communication with Camel routes. This means that the Citrus producer sends the exchange message to the route endpoint uri and is finished immediately. There is no synchronous response to await. In contrary to that the synchronous endpoint will send and receive a synchronous message on the Camel destination route. This message exchange pattern is discussed in a later section in this chapter.
For now, we have a look on how to use the Citrus Camel endpoint in a test case in order to send a message to the Camel route:
send(helloCamelEndpoint)
.message()
.body("Hello from Citrus!");<send endpoint="helloCamelEndpoint">
<message type="plaintext">
<payload>Hello from Citrus!</payload>
</message>
</send>You can use the very same Citrus Camel endpoint component to receive messages in your test case, too. In this situation you would receive a message from the route endpoint. This is especially designed for queueing endpoint routes such as the Camel seda component. In our example Camel route above the seda Camel component is called with the endpoint uri seda:greetings-feed.
This means that the Camel route is sending a message to the seda component.
Citrus is able to receive this route message with an endpoint component like this:
@Bean
public CamelEndpoint greetingsFeed() {
return new CamelEndpointBuilder()
.endpointUri("seda:greetings-feed")
.build();
}<citrus-camel:endpoint id="greetingsFeed"
endpoint-uri="seda:greetings-feed"/>You can use the Citrus camel endpoint in your test case receive action in order to consume the message on the seda component.
receive(greetingsFeed)
.message()
.type(MessageType.PLAINTEXT)
.body("Hello from Citrus!");<receive endpoint="greetingsFeed">
<message type="plaintext">
<payload>Hello from Citrus!</payload>
</message>
</receive>|
Tip
|
Instead of defining a static Citrus camel component you could also use the dynamic endpoint components in Citrus. This would enable you to send your message directly using the endpoint uri direct:news in your test case. Read more about this in dynamic-endpoint-components. |
Citrus is able to send and receive messages with Camel route endpoint uri. This enables you to invoke a Camel route. The Camel components used is defined by the endpoint uri as usual. When interacting with Camel routes you might need to send back some response messages in order to simulate boundary applications. We will discuss the synchronous communication in the next section.
The synchronous Camel producer sends a message to a route and waits synchronously for the response to arrive. In Camel this communication is represented with the exchange pattern InOut. The basic configuration for a synchronous Camel endpoint component looks like follows:
@Bean
public CamelSyncEndpoint helloCamelEndpoint() {
return new CamelSyncEndpointBuilder()
.endpointUri("direct:hello")
.timeout(1000L)
.pollingInterval(300L)
.build();
}<citrus-camel:sync-endpoint id="helloCamelEndpoint"
endpoint-uri="direct:hello"
timeout="1000"
polling-interval="300"/>Synchronous endpoints poll for synchronous reply messages to arrive. The poll interval is an optional setting in order to manage the amount of reply message handshake attempts. Once the endpoint was able to receive the reply message synchronously the test case can receive the reply. In case the reply message is not available in time we raise some timeout error and the test will fail.
In a first test scenario we write a test case that sends a message to the synchronous endpoint and waits for the synchronous reply message to arrive. So we have two actions on the same Citrus endpoint, first send then receive.
send(helloCamelEndpoint)
.message()
.type(MessageType.PLAINTEXT)
.body("Hello from Citrus!");
receive(helloCamelEndpoint)
.message()
.type(MessageType.PLAINTEXT)
.body("This is the reply from Apache Camel!");<send endpoint="helloCamelEndpoint">
<message type="plaintext">
<payload>Hello from Citrus!</payload>
</message>
</send>
<receive endpoint="helloCamelEndpoint">
<message type="plaintext">
<payload>This is the reply from Apache Camel!</payload>
</message>
</receive>The next variation deals with the same synchronous communication, but send and receive roles are switched. Now Citrus receives a message from a Camel route and has to provide a reply message. We handle this synchronous communication with the same synchronous Apache Camel endpoint component. Only difference is that we initially start the communication by receiving a message from the endpoint. Knowing this Citrus is able to send a synchronous response back. Again just use the same endpoint reference in your test case. So we have again two actions in our test case, but this time first receive then send.
receive(helloCamelEndpoint)
.message()
.type(MessageType.PLAINTEXT)
.body("Hello from Apache Camel!");
send(helloCamelEndpoint)
.message()
.type(MessageType.PLAINTEXT)
.body("This is the reply from Citrus!");<receive endpoint="helloCamelEndpoint">
<message type="plaintext">
<payload>Hello from Apache Camel!</payload>
</message>
</receive>
<send endpoint="helloCamelEndpoint">
<message type="plaintext">
<payload>This is the reply from Citrus!</payload>
</message>
</send>This is pretty simple. Citrus takes care of setting the Camel exchange pattern InOut while using synchronous communications. The Camel routes do respond and Citrus is able to receive the synchronous messages accordingly. With this pattern you can interact with Camel routes where Citrus simulates synchronous clients and consumers.
Camel uses exchanges when sending and receiving messages to and from routes. These exchanges hold specific information on the communication outcome. Citrus automatically converts these exchange information to special message header entries. You can validate those exchange headers then easily in your test case:
receive(greetingsFeed)
.message()
.type(MessageType.PLAINTEXT)
.body("Hello from Camel!")
.header("citrus_camel_route_id", "greetings")
.header("citrus_camel_exchange_id", "ID-local-50532-1402653725341-0-3")
.header("citrus_camel_exchange_failed", false)
.header("citrus_camel_exchange_pattern", "InOnly")
.header("CamelCorrelationId", "ID-local-50532-1402653725341-0-1")
.header("CamelToEndpoint", "seda://greetings-feed");<receive endpoint="greetingsFeed">
<message type="plaintext">
<payload>Hello from Camel!</payload>
</message>
<header>
<element name="citrus_camel_route_id" value="greetings"/>
<element name="citrus_camel_exchange_id" value="ID-local-50532-1402653725341-0-3"/>
<element name="citrus_camel_exchange_failed" value="false"/>
<element name="citrus_camel_exchange_pattern" value="InOnly"/>
<element name="CamelCorrelationId" value="ID-local-50532-1402653725341-0-1"/>
<element name="CamelToEndpoint" value="seda://greetings-feed"/>
</header>
</receive>In addition to the Camel specific exchange information the Camel exchange does also hold some custom properties.
These properties such as CamelToEndpoint or CamelCorrelationId are also added automatically to the Citrus message header so can expect them in a receive message action.
Let us suppose the following route definition:
@Bean
public CamelContext camelContext() {
CamelContext context = new DefaultCamelContext();
context.addRoutes(new RouteBuilder() {
@Override
public void configure() throws Exception {
from("direct:hello")
.routeId("helloRoute")
.to("log:org.citrusframework.camel?level=INFO")
.to("seda:greetings-feed")
.onException(CitrusRuntimeException.class)
.to("seda:exceptions");
}
});
return context;
}<camelContext id="camelContext" xmlns="http://camel.apache.org/schema/spring">
<route id="helloRoute">
<from uri="direct:hello"/>
<to uri="log:org.citrusframework.camel?level=INFO"/>
<to uri="seda:greetings-feed"/>
<onException>
<exception>org.citrusframework.exceptions.CitrusRuntimeException</exception>
<to uri="seda:exceptions"/>
</onException>
</route>
</camelContext>The route has an exception handling block defined that is called as soon as the exchange processing ends up in some error or exception. With Citrus you can also simulate an exchange exception when sending back a synchronous response to a calling route.
send(helloCamelEndpoint)
.message()
.type(MessageType.PLAINTEXT)
.body("Something went wrong!")
.header("citrus_camel_exchange_exception", CitrusRuntimeException.class)
.header("citrus_camel_exchange_exception_message", "Something went wrong!")
.header("citrus_camel_exchange_failed", true);<send endpoint="greetingsFeed">
<message type="plaintext">
<payload>Something went wrong!</payload>
</message>
<header>
<element name="citrus_camel_exchange_exception"
value="org.citrusframework.exceptions.CitrusRuntimeException"/>
<element name="citrus_camel_exchange_exception_message" value="Something went wrong!"/>
<element name="citrus_camel_exchange_failed" value="true"/>
</header>
</send>This message as response to the seda:greetings-feed route would cause Camel to enter the exception handling in the route definition. The exception handling is activated and calls the error handling route endpoint seda:exceptions . Of course Citrus would be able to receive such an exception exchange validating the exception handling outcome.
In such failure scenarios the Camel exchange holds the exception information (CamelExceptionCaught) such as causing exception class and error message. These headers are present in an error scenario and can be validated in Citrus when receiving error messages as follows:
receive(errorCamelEndpoint)
.message()
.type(MessageType.PLAINTEXT)
.body("Something went wrong!")
.header("citrus_camel_route_id", "helloRoute")
.header("citrus_camel_exchange_failed", true)
.header("CamelExceptionCaught", "org.citrusframework.exceptions.CitrusRuntimeException: Something went wrong!");<receive endpoint="errorCamelEndpoint">
<message type="plaintext">
<payload>Something went wrong!</payload>
</message>
<header>
<element name="citrus_camel_route_id" value="helloRoute"/>
<element name="citrus_camel_exchange_failed" value="true"/>
<element name="CamelExceptionCaught"
value="org.citrusframework.exceptions.CitrusRuntimeException: Something went wrong!"/>
</header>
</receive>This completes the basic exception handling in Citrus when using the Camel endpoints.
In the previous samples we have used the Camel context as Spring bean context that is automatically loaded when Citrus starts up.
Now when using a single Camel context instance Citrus is able to automatically pick this Camel context for route interaction.
If you use more than one Camel context you have to tell the Citrus endpoint component which context to use.
The endpoint offers an optional attribute called camel-context.
@Bean
public CamelEndpoint newsCamelEndpoint() {
return new CamelEndpointBuilder()
.camelContext(newsContext())
.endpointUri("direct:news")
.build();
}
@Bean
public CamelContext newsContext() {
CamelContext context = new DefaultCamelContext();
context.addRoutes(new RouteBuilder() {
@Override
public void configure() throws Exception {
from("direct:news")
.routeId("newsRoute")
.to("log:org.citrusframework.camel?level=INFO")
.to("seda:news-feed");
}
});
return context;
}
@Bean
public CamelContext helloContext() {
CamelContext context = new DefaultCamelContext();
context.addRoutes(new RouteBuilder() {
@Override
public void configure() throws Exception {
from("direct:hello")
.routeId("helloRoute")
.to("log:org.citrusframework.camel?level=INFO")
.to("seda:greetings");
}
});
return context;
}<citrus-camel:endpoint id="newsCamelEndpoint"
camel-context="newsContext"
endpoint-uri="direct:news"/>
<camelContext id="newsContext" xmlns="http://camel.apache.org/schema/spring">
<route id="newsRoute">
<from uri="direct:news"/>
<to uri="log:org.citrusframework.camel?level=INFO"/>
<to uri="seda:news-feed"/>
</route>
</camelContext>
<camelContext id="helloContext" xmlns="http://camel.apache.org/schema/spring">
<route id="helloRoute">
<from uri="direct:hello"/>
<to uri="log:org.citrusframework.camel?level=INFO"/>
<to uri="seda:greetings"/>
</route>
</camelContext>In the example above we have two Camel context instances loaded. The endpoint has to pick the context to use with the attribute camel-context which resides to the Spring bean id of the Camel context.
Since Citrus 2.4 we introduced some Camel specific test actions that enable easy interaction with Camel routes and the Camel context.
|
Note
|
In XML the Camel route test actions do follow a specific XML namespace. This means you have to add this namespace to the test case when using the actions. |
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:camel="http://www.citrusframework.org/schema/camel/testcase"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.citrusframework.org/schema/camel/testcase
http://www.citrusframework.org/schema/camel/testcase/citrus-camel-testcase.xsd">
[...]
</beans>Once you have added the special Camel namespace with prefix camel you are ready to start using the Camel test actions in your test case.
You can create a new Camel route as part of the test using this test action.
public class CamelRouteActionIT extends TestNGCitrusSpringSupport {
@Autowired
private CamelContext camelContext;
@Test
@CitrusTest
public void createCamelRoute() {
$(camel().camelContext(camelContext)
.route()
.create(new RouteBuilder() {
@Override
public void configure() throws Exception {
from("direct:messages")
.routeId("message-tokenizer")
.split().tokenize(" ")
.to("seda:words");
}
}));
}
}<testcase name="CamelRouteIT">
<actions>
<camel:create-routes>
<routeContext xmlns="http://camel.apache.org/schema/spring">
<route id="message-tokenizer">
<from uri="direct:messages"/>
<split>
<tokenize token=" "/>
<to uri="seda:words"/>
</split>
</route>
</routeContext>
</camel:create-routes>
</actions>
</testcase>In the example above we have used the camel:create-route test action that will create new Camel routes at runtime in the Camel context. The target Camel context is referenced with an automatic context lookup.
|
Note
|
The default Camel context name in this lookup is "citrusCamelContext". You can set this default context name via environment variables (CITRUS_CAMEL_CONTEXT_NAME) or system properties (citrus.camel.context.name). |
If no specific settings are set Citrus will automatically try to look up the Camel context with name "citrusCamelContext" in the Spring bean configuration. All route operations will target this Camel context then.
In addition to that you can skip this lookup and directly reference a target Camel context with the action attribute camel-context (used in the second action above).
You can remove routes from the Camel context as part of the test.
public class CamelRouteActionIT extends TestNGCitrusSpringSupport {
@Autowired
private CamelContext camelContext;
@Test
@CitrusTest
public void removeCamelRoutes() {
$(camel().camelContext(camelContext)
.route()
.remove("route_1", "route_2", "route_3"));
}
}<testcase name="CamelRouteIT">
<actions>
<camel:remove-routes camel-context="camelContext">
<route id="route_1"/>
<route id="route_2"/>
<route id="route_3"/>
</camel:remove-routes>
</actions>
</testcase>Next operation we will discuss is the start and stop of existing Camel routes:
public class CamelRouteActionIT extends TestNGCitrusSpringSupport {
@Autowired
private CamelContext camelContext;
@Test
@CitrusTest
public void startThenStopCamelRoutes() {
$(camel().camelContext(camelContext)
.route()
.start("route_1"));
$(camel().camelContext(camelContext)
.route()
.stop("route_2", "route_3"));
}
}<testcase name="CamelRouteIT">
<actions>
<camel:start-routes camel-context="camelContext">
<route id="route_1"/>
</camel:start-routes>
<camel:stop-routes camel-context="camelContext">
<route id="route_2"/>
<route id="route_3"/>
</camel:stop-routes>
</actions>
</testcase>Starting and stopping Camel routes at runtime is important when temporarily Citrus need to receive a message on a Camel endpoint URI. We can stop a route, use a Citrus camel endpoint instead for validation and start the route after the test is done. This way we can also simulate errors and failure scenarios in a Camel route interaction.
The Camel controlbus component is a good way to access route statistics and route status information within a Camel context. Citrus provides controlbus test actions to easily access the controlbus operations at runtime.
public class CamelControlBusIT extends TestNGCitrusSpringSupport {
@Autowired
private CamelContext camelContext;
@Test
@CitrusTest
public void createCamelRoute() {
$(camel().camelContext(camelContext)
.controlbus()
.route("route_1")
.status()
.result(ServiceStatus.Stopped));
$(camel().camelContext(camelContext)
.controlbus()
.route("route_1")
.start());
$(camel().camelContext(camelContext)
.controlbus()
.route("route_1")
.status()
.result(ServiceStatus.Started));
}
}<testcase name="CamelControlBusIT">
<actions>
<camel:control-bus camel-context="camelContext">
<camel:route id="route_1" action="status"/>
<camel:result>Stopped</camel:result>
</camel:control-bus>
<camel:control-bus>
<camel:route id="route_1" action="start"/>
</camel:control-bus>
<camel:control-bus camel-context="camelContext">
<camel:route id="route_1" action="status"/>
<camel:result>Started</camel:result>
</camel:control-bus>
<camel:control-bus>
<camel:language type="simple">${camelContext.stop()}</camel:language>
</camel:control-bus>
<camel:control-bus camel-context="camelContext">
<camel:language type="simple">${camelContext.getRouteController().getRouteStatus('route_3')}</camel:language>
<camel:result>Started</camel:result>
</camel:control-bus>
</actions>
</testcase>The example test case shows the controlbus access.
As already mentioned you can explicitly reference a target Camel context with camel-context="camelContext".
In case no specific context is referenced Citrus will automatically lookup a target Camel context with the default context name "citrusCamelContext".
Camel provides two different ways to specify operations and parameters. The first option is the use of an action attribute. The Camel route id has to be specified as mandatory attribute. As a result the controlbus action will be executed on the target route during test runtime. This way we can also start and stop Camel routes in a Camel context.
In case a controlbus operation has a result such as the status action we can specify a control result that is compared. Citrus will raise validation exceptions when the results differ.
The second option for executing a controlbus action is the language expression. We can use Camel language expressions on the Camel context for accessing a controlbus operation. Also, here we can define an optional outcome as expected result.
public class CamelControlBusIT extends TestNGCitrusSpringSupport {
@Autowired
private CamelContext camelContext;
@Test
@CitrusTest
public void createCamelRoute() {
$(camel().camelContext(camelContext)
.controlbus()
.language("simple", "${camelContext.getRouteStatus('my_route')}")
.result(ServiceStatus.Stopped));
$(camel().camelContext(camelContext)
.controlbus()
.language("simple", "${camelContext.stop()}"));
}
}<testcase name="CamelControlBusIT">
<actions>
<camel:control-bus camel-context="camelContext">
<camel:language type="simple">${camelContext.getRouteStatus('my_route')}</camel:language>
<camel:result>Started</camel:result>
</camel:control-bus>
<camel:control-bus>
<camel:language type="simple">${camelContext.stop()}</camel:language>
</camel:control-bus>
</actions>
</testcase>The Apache Camel project offers a great CLI tooling that allows you to run Camel integrations directly from the command line. Citrus supports two ways to launch the Camel CLI:
-
JBang (default) — uses JBang to resolve and run the Camel CLI application.
-
Camel Launcher jar — uses a standalone Camel Launcher jar file executed via
java -jar.
By default, Citrus uses JBang to launch the Camel CLI. You can install the Camel CLI tooling on your local machine as a JBang application.
jbang app install camel@apache/camelThe --help option will show you all available commands that you can run with Camel CLI.
As an alternative to JBang you can use the Camel Launcher jar to run Camel CLI commands. The Camel Launcher is a standalone executable jar that does not require JBang to be installed.
To use the Camel Launcher jar, configure the following settings:
citrus.camel.cli.type=launcher
citrus.camel.cli.launcher.jar.path=/path/to/camel-launcher.jarexport CITRUS_CAMEL_CLI_TYPE=launcher
export CITRUS_CAMEL_CLI_LAUNCHER_JAR_PATH=/path/to/camel-launcher.jarWhen configured, Citrus will execute Camel CLI commands as java -jar <path-to-jar> <command> <args> instead of using JBang.
One of the available commands in Camel CLI is the run command that takes a Camel route as an input and runs the route locally without any prior project setup.
Citrus is able to use this capability in a test to run a Camel integration in a separate process.
Given the following Camel route written in YAML:
- from:
uri: "timer:tick"
parameters:
period: "1000"
includeMetadata: true
steps:
- setBody:
simple: "{{greeting}} #${header.CamelTimerCounter}"
- to: "log:info"The Camel route uses a timer component to periodically log a message to the output.
The message is given as a property placeholder greeting and may be set in the application.properties.
greeting=Hello from Apache Camel!The Citrus test is able to run this route with Camel CLI in a separate process:
public class CamelCliTest extends TestNGCitrusSupport implements TestActionSupport {
@Test
@CitrusTest
public void shouldRunCamelCli() {
when(camel().cli()
.run()
.integration(Resources.fromClasspath("route.yaml", CamelCliTest.class))
.withSystemProperty("greeting", "Hello from Apache Camel!"));
}
}name: "CamelCliTest"
actions:
- camel:
cli:
run:
integration:
file: "classpath:route.yaml"
systemProperties:
properties:
- name: greeting
value: Hello from Apache Camel!<test name="CamelCliTest" xmlns="http://citrusframework.org/schema/xml/testcase"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://citrusframework.org/schema/xml/testcase http://citrusframework.org/schema/xml/testcase/citrus-testcase.xsd">
<actions>
<camel>
<cli>
<run>
<integration file="classpath:route.yaml">
<system-properties>
<property name="greeting" value="Hello from Apache Camel!"/>
</system-properties>
</integration>
</run>
</cli>
</camel>
</actions>
</test><spring:beans xmlns="http://www.citrusframework.org/schema/testcase"
xmlns:spring="http://www.springframework.org/schema/beans">
<!-- NOT SUPPORTED -->
</spring:beans>This starts the Camel integration with Camel CLI.
The test waits for the integration to report running status.
You can skip the waiting state with the waitForRunningState=false option.
|
Tip
|
The test action has an autoRemove option that is enabled by default. This means that the test will automatically stop and remove the Camel integration after the test.
|
|
Tip
|
You may choose to add the --stub option to the Camel CLI run command. This allows you to stub endpoints in the Camel integration route. The stubbed endpoint uses a local internal message queue instead of connecting to the real endpoint. This is very useful when an endpoint raises quite complex connection efforts. For instance, you may stub an SQL endpoint or a message broker connection endpoint such as Kafka or JMS when running the Camel integration. You may access the messages processed by the stubbed endpoint in the test with a cmd receive test action that is described later in this guide.
|
You may want to explicitly stop the Camel integration. You can do this with the following action:
public class CamelCliTest extends TestNGCitrusSupport implements TestActionSupport {
@Test
@CitrusTest
public void shouldRunCamelCli() {
when(camel().cli()
.stop()
.integration("route")
);
}
}name: "CamelCliTest"
actions:
- camel:
cli:
stop:
integration: "route"<test name="CamelCliTest" xmlns="http://citrusframework.org/schema/xml/testcase"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://citrusframework.org/schema/xml/testcase http://citrusframework.org/schema/xml/testcase/citrus-testcase.xsd">
<actions>
<camel>
<cli>
<stop integration="route"/>
</cli>
</camel>
</actions>
</test><spring:beans xmlns="http://www.citrusframework.org/schema/testcase"
xmlns:spring="http://www.springframework.org/schema/beans">
<!-- NOT SUPPORTED -->
</spring:beans>The Camel integration to stop is identified by its name. Usually this is the file name without the file extension. You can also specify a name when running the integration and reference this name in the stop action.
Each Camel integration reports a state (running or stopped) that Citrus is able to verify. In addition to that the running Camel integration produces a log output that Citrus is able to scan for expected messages.
You can do all of this in the verify action.
public class CamelCliTest extends TestNGCitrusSupport implements TestActionSupport {
@Test
@CitrusTest
public void shouldRunCamelCli() {
when(camel().cli()
.verify("route")
.waitForLogMessage("Hello from Apache Camel! #10"));
}
}name: "CamelCliTest"
actions:
- camel:
cli:
verify:
integration: "route"
logMessage: "Hello from Apache Camel! #10"<test name="CamelCliTest" xmlns="http://citrusframework.org/schema/xml/testcase"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://citrusframework.org/schema/xml/testcase http://citrusframework.org/schema/xml/testcase/citrus-testcase.xsd">
<actions>
<camel>
<cli>
<verify integration="route" log-message="Hello from Apache Camel! #10"/>
</cli>
</camel>
</actions>
</test><spring:beans xmlns="http://www.citrusframework.org/schema/testcase"
xmlns:spring="http://www.springframework.org/schema/beans">
<!-- NOT SUPPORTED -->
</spring:beans>The action above verifies that the Camel integration is running. Also, the action waits for a log message Hello from Apache Camel! #10.
The action periodically polls the produced log output and searches for the expected log message.
The action provides options such as maxAttempts and pollingInterval to stop the waiting period. In case the log message is not found after the maximum number of attempts the test action fails the test.
The Camel CLI tooling allows you to send messages to running Camel integrations.
You can use the send command to send messages to any running route.
Give the following Camel integration that is running in Camel CLI:
- from:
uri: "direct:echo"
steps:
- transform:
simple: "${body.toUpperCase()}"
- to: "log:info"The Camel route listens for messages on the direct:echo endpoint.
With Camel CLI you can send messages to this route using the following command:
public class CamelCliTest extends TestNGCitrusSupport implements TestActionSupport {
@Test
@CitrusTest
public void shouldSendMessage() {
when(camel().cli().cmd()
.send()
.integration("echo")
.body("Hello Camel"));
}
}name: "CamelCliTest"
actions:
- camel:
cli:
cmd:
send:
integration: "echo"
body:
data: "Hello Camel"<test name="CamelCliTest" xmlns="http://citrusframework.org/schema/xml/testcase"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://citrusframework.org/schema/xml/testcase http://citrusframework.org/schema/xml/testcase/citrus-testcase.xsd">
<actions>
<camel>
<cli>
<cmd>
<send integration="echo">
<body>
<data>Hello Camel</data>
</body>
</send>
</cmd>
</cli>
</camel>
</actions>
</test><spring:beans xmlns="http://www.citrusframework.org/schema/testcase"
xmlns:spring="http://www.springframework.org/schema/beans">
<!-- NOT SUPPORTED -->
</spring:beans>The send action connects with the running Camel integration called echo and send the message body Hello Camel to the route.
The send operation automatically uses the direct:echo endpoint because there is only one single route.
In case you have multiple routes running with multiple endpoints you can specify the endpoint with the endpoint attribute.
Also, you may set some additional message headers.
public class CamelCliTest extends TestNGCitrusSupport implements TestActionSupport {
@Test
@CitrusTest
public void shouldSendMessage() {
when(camel().cli().cmd()
.send()
.integration("echo")
.endpoint("direct:echo")
.header("user", "christoph")
.body("Hello Camel"));
}
}name: "CamelCliTest"
actions:
- camel:
cli:
cmd:
send:
integration: "echo"
endpoint: "direct:echo"
headers:
user: christoph
body:
data: "Hello Camel"<test name="CamelCliTest" xmlns="http://citrusframework.org/schema/xml/testcase"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://citrusframework.org/schema/xml/testcase http://citrusframework.org/schema/xml/testcase/citrus-testcase.xsd">
<actions>
<camel>
<cli>
<cmd>
<send integration="echo" endpoint="direct:echo">
<headers>
<header name="user" value="christoph"/>
</headers>
<body>
<data>Hello Camel</data>
</body>
</send>
</cmd>
</cli>
</camel>
</actions>
</test><spring:beans xmlns="http://www.citrusframework.org/schema/testcase"
xmlns:spring="http://www.springframework.org/schema/beans">
<!-- NOT SUPPORTED -->
</spring:beans>You can specify a send timeout with the timeout attribute (by default this is set to 20 seconds).
The reply=true/false option makes the send operation wait for a response message from the Camel integration.
The response data is logged to the output.
You can also use the Camel CLI send command without a running Camel integration. This can be used to send messages to any support Camel endpoint. Just use the endpoint uri setting on the send operation like this:
public class CamelCliTest extends TestNGCitrusSupport implements TestActionSupport {
@Test
@CitrusTest
public void shouldSendMessage() {
when(camel().cli().cmd()
.send()
.uri("kafka:echo?server=localhost:9092")
.header("user", "christoph")
.body("Hello Camel"));
}
}name: "CamelCliTest"
actions:
- camel:
cli:
cmd:
send:
uri: "kafka:echo?server=localhost:9092"
headers:
user: christoph
body:
data: "Hello Camel"<test name="CamelCliTest" xmlns="http://citrusframework.org/schema/xml/testcase"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://citrusframework.org/schema/xml/testcase http://citrusframework.org/schema/xml/testcase/citrus-testcase.xsd">
<actions>
<camel>
<cli>
<cmd>
<send uri="kafka:echo?server=localhost:9092">
<headers>
<header name="user" value="christoph"/>
</headers>
<body>
<data>Hello Camel</data>
</body>
</send>
</cmd>
</cli>
</camel>
</actions>
</test><spring:beans xmlns="http://www.citrusframework.org/schema/testcase"
xmlns:spring="http://www.springframework.org/schema/beans">
<!-- NOT SUPPORTED -->
</spring:beans>|
Tip
|
You can also use the Kamelets sources to send messages with Camel CLI. The Kamelets provide easy to use properties for comfortable connection options. |
Please see the Camel CLI send command help for more configuration options.
camel cmd send --helpThe Camel CLI tooling allows you to access stubbed messages in a running Camel integration.
You can use the receive command to list messages that an endpoint in Camel has been processing.
Give the following Camel integration that is running in Camel CLI:
- from:
uri: "direct:echo"
steps:
- transform:
simple: "${body.toUpperCase()}"
- to: "kafka:inbox?brokers=localhost:9092"The Camel route listens for messages on the direct:echo endpoint and connects with a Kafka message broker.
With Camel CLI you can start the route with a stubbed Kafka endpoint like this:
camel run my-route.yaml --stub=kafkaThis makes Camel use an internal message queue instead of connecting with the Kafka message broker.
The messages received by the stubbed Kafka component in Camel can be accessed with the receive command:
public class CamelCliTest extends TestNGCitrusSupport implements TestActionSupport {
@Test
@CitrusTest
public void shouldSendMessage() {
then(camel().cli().cmd()
.receive()
.integration("my-route")
.tail(1)
.endpoint("kafka:inbox"));
}
}name: "CamelCliTest"
actions:
- camel:
cli:
cmd:
receive:
integration: "my-route"
tail: 1
endpoint: "kafka:inbox"<test name="CamelCliTest" xmlns="http://citrusframework.org/schema/xml/testcase"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://citrusframework.org/schema/xml/testcase http://citrusframework.org/schema/xml/testcase/citrus-testcase.xsd">
<actions>
<camel>
<cli>
<cmd>
<receive integration="my-route" tail="1" endpoint="kafka:inbox"/>
</cmd>
</cli>
</camel>
</actions>
</test><spring:beans xmlns="http://www.citrusframework.org/schema/testcase"
xmlns:spring="http://www.springframework.org/schema/beans">
<!-- NOT SUPPORTED -->
</spring:beans>The receive action connects with the running Camel integration called my-route and receives messages on the stubbed kafka:inbox endpoint.
The tail setting defines how many messages that have been received before will be included.
This means a setting of tail=1 includes the last message that was processed by the stubbed endpoint.
By default, the operation will wait only for new messages to arrive (tail=0).
The test action monitors the receive command output and waits for messages to arrive.
You can use the grep option to wait for very specific messages that match the given expression.
Also, you can specify the since option that allows you to include messages that have arrived in a given time span (e.g. 5s, 2m, 1h)
public class CamelCliTest extends TestNGCitrusSupport implements TestActionSupport {
@Test
@CitrusTest
public void shouldSendMessage() {
then(camel().cli().cmd()
.receive()
.integration("my-route")
.tail(1)
.since("1h")
.grep("HELLO CAMEL")
.endpoint("kafka:inbox"));
}
}name: "CamelCliTest"
actions:
- camel:
cli:
cmd:
receive:
integration: "my-route"
tail: 1
since: 1h
grep: HELLO CAMEL
endpoint: "kafka:inbox"<test name="CamelCliTest" xmlns="http://citrusframework.org/schema/xml/testcase"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://citrusframework.org/schema/xml/testcase http://citrusframework.org/schema/xml/testcase/citrus-testcase.xsd">
<actions>
<camel>
<cli>
<cmd>
<receive integration="my-route"
tail="1"
since="1h"
grep="HELLO CAMEL"
endpoint="kafka:inbox"/>
</cmd>
</cli>
</camel>
</actions>
</test><spring:beans xmlns="http://www.citrusframework.org/schema/testcase"
xmlns:spring="http://www.springframework.org/schema/beans">
<!-- NOT SUPPORTED -->
</spring:beans>The grep, tail and since options help you to verify that a specific message has been processed on the stubbed endpoint.
The test action polls the command output for the message and throws a timeout error in case the message does not arrive in time.
You can use the maxAttempts and delayBetweeAttempts settings to specify the waiting and polling configuration.
Please see the Camel CLI receive command help for more configuration options.
camel cmd receive --helpCamel CLI supports plugins that extend the CLI with additional commands and capabilities. Citrus provides test actions to add and delete plugins as part of a test.
when(camel().cli()
.plugin()
.add()
.pluginName("my-plugin")
.withArg("--version", "1.0.0"));- camel:
cli:
plugin:
add:
name: "my-plugin"
args:
- "--version"
- "1.0.0"<camel>
<cli>
<plugin>
<add name="my-plugin" args="--version 1.0.0"/>
</plugin>
</cli>
</camel>The action checks if the plugin is already installed by name.
When the plugin is already installed and no --version or --gav arguments are provided, the action skips the installation.
When --version or --gav arguments are present, the action removes the existing plugin and reinstalls it with the new arguments.
|
Tip
|
The test action has an autoRemove option that is disabled by default. When enabled, the test will automatically delete the plugin after the test using the context.doFinally() mechanism. You can enable it globally via the citrus.camel.cli.auto.remove.plugins system property or CITRUS_CAMEL_CLI_AUTO_REMOVE_PLUGINS environment variable.
|
You can explicitly delete a plugin in your test.
This is useful in finally blocks to clean up plugins that were installed during the test.
when(camel().cli()
.plugin()
.delete()
.pluginName("my-plugin"));- camel:
cli:
plugin:
delete:
name: "my-plugin"<camel>
<cli>
<plugin>
<delete name="my-plugin"/>
</plugin>
</cli>
</camel>The Camel CLI support uses the following environment settings.
Settings use the citrus.camel.cli. prefix. The older citrus.camel.jbang. prefix is still supported as a fallback.
| System property | Description |
|---|---|
citrus.camel.cli.type |
The launcher type to use: |
citrus.camel.cli.launcher.jar.path |
Path to the Camel Launcher jar file. Required when |
citrus.camel.cli.app |
Camel JBang app name. (default="camel@apache/camel") |
citrus.camel.cli.version |
Camel version to use. (default="latest") |
citrus.camel.cli.work.dir |
Local working directory for the Camel CLI. |
citrus.camel.cli.kamelets.version |
Kamelets version used by the Camel CLI runtime. |
citrus.camel.cli.kamelets.local.dir |
Local Kamelets directory. |
citrus.camel.cli.dump.integration.output |
When set to true, Camel CLI process output for integrations will be redirected to a file in the current working directory. (default="false") |
citrus.camel.cli.verbose |
When set to true, Camel CLI will print detailed messages. (default="false") |
citrus.camel.cli.auto.remove.resources |
When set to true, Camel CLI resources created during the test are automatically removed after the test. (default="true") |
citrus.camel.cli.auto.remove.plugins |
When set to true, Camel CLI plugins added during the test are automatically removed after the test. (default="false") |
citrus.camel.cli.wait.for.running.state |
When set to true, Camel CLI will automatically wait for each integration created to be in running state. (default="true") |
citrus.camel.cli.trust.url |
Comma-separated list of trust URLs for JBang. (default="https://github.com/apache/camel/") |
| Environment variable | Description |
|---|---|
CITRUS_CAMEL_CLI_TYPE |
The launcher type to use: |
CITRUS_CAMEL_CLI_LAUNCHER_JAR_PATH |
Path to the Camel Launcher jar file. |
CITRUS_CAMEL_CLI_APP |
Camel JBang app name. |
CITRUS_CAMEL_CLI_VERSION |
Camel version to use. |
CITRUS_CAMEL_CLI_WORK_DIR |
Local working directory for the Camel CLI. |
CITRUS_CAMEL_CLI_KAMELETS_VERSION |
Kamelets version used by the Camel CLI runtime. |
CITRUS_CAMEL_CLI_KAMELETS_LOCAL_DIR |
Local Kamelets directory. |
CITRUS_CAMEL_CLI_DUMP_INTEGRATION_OUTPUT |
When set to true, integration output is redirected to a file. |
CITRUS_CAMEL_CLI_VERBOSE |
When set to true, Camel CLI will print detailed messages. |
CITRUS_CAMEL_CLI_AUTO_REMOVE_RESOURCES |
When set to true, resources are automatically removed after the test. |
CITRUS_CAMEL_CLI_AUTO_REMOVE_PLUGINS |
When set to true, plugins are automatically removed after the test. |
CITRUS_CAMEL_CLI_WAIT_FOR_RUNNING_STATE |
When set to true, waits for integrations to reach running state. |
CITRUS_CAMEL_CLI_TRUST_URL |
Comma-separated list of trust URLs for JBang. |
The Camel CLI integrations are run locally by default as a separate JBang process. In addition to that you may also run the integration in a Kubernetes namespace.
This uses the Camel CLI Kubernetes plugin that is able to build a container image and deploy it to a connected Kubernetes namespace.
The Camel CLI Kubernetes plugin is able to build a container image from an integration and deploy the integration with a generated Kubernetes manifest.
|
Note
|
The following test actions call the Camel Kubernetes plugin. A prerequisite to this is that you are connected to a Kubernetes cluster and a namespace. It is essential to have a Kubernetes command line tooling installed and ready for usage on your local machine. |
Citrus provides a test action to call the Camel CLI Kubernetes plugin:
public class CamelCliTest extends TestNGCitrusSupport implements TestActionSupport {
@Test
@CitrusTest
public void shouldRunCamelCli() {
when(camel().cli()
.kubernetes()
.run()
.integration(Resources.fromClasspath("route.yaml", CamelCliTest.class))
.runtime("quarkus")
.clusterType("kind")
.imageBuilder("docker")
.imageRegistry("localhost:5000")
.withProperty("greeting=Hello from Apache Camel!")
);
}
}name: "CamelCliTest"
actions:
- camel:
cli:
kubernetes:
run:
integration:
file: "classpath:route.yaml"
runtime: "quarkus"
clusterType: "kind"
imageBuilder: "docker"
imageRegistry: "localhost:5000"
properties:
- name: greeting
value: Hello from Apache Camel!<test name="CamelCliTest" xmlns="http://citrusframework.org/schema/xml/testcase"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://citrusframework.org/schema/xml/testcase http://citrusframework.org/schema/xml/testcase/citrus-testcase.xsd">
<actions>
<camel>
<cli>
<kubernetes>
<run runtime="quarkus"
cluster-type="kind"
image-builder="docker"
image-registry="localhost:5000">
<integration file="classpath:route.yaml"/>
<properties>
<property name="greeting" value="Hello from Apache Camel!"/>
</properties>
</run>
</kubernetes>
</cli>
</camel>
</actions>
</test><spring:beans xmlns="http://www.citrusframework.org/schema/testcase"
xmlns:spring="http://www.springframework.org/schema/beans">
<!-- NOT SUPPORTED -->
</spring:beans>The Kubernetes run action receives detailed configuration on how to build the container image and where to push the resulting image.
You may choose the target cluster type and use an external or internal image registry (e.g. quay.io or localhost:5000).
The Camel CLI Kubernetes plugin will perform several steps to create the deployment. First of all the plugin creates a project export for the given Camel integration and builds the container image.
Also, the plugin generates a Kubernetes manifest that holds all required resources to deploy the integration to a Kubernetes cluster. The Kubernetes deployment is automatically performed as a last step in this action.
The result will be a new Kubernetes deployment in the current namespace that uses the freshly built container image.
|
Important
|
Please make sure to connect to a Kubernetes cluster and choose a proper namespace before running the test action. The Kubernetes client will automatically use the current cluster and namespace. |
|
Tip
|
The action has an autoRemove setting that is enabled by default. This means that the Kubernetes deployment is automatically stopped and removed after the test.
|
You can explicitly remove the Camel integration from Kubernetes.
public class CamelCliTest extends TestNGCitrusSupport implements TestActionSupport {
@Test
@CitrusTest
public void shouldRunCamelCli() {
when(camel().cli()
.kubernetes()
.delete()
.integration("route")
);
}
}name: "CamelCliTest"
actions:
- camel:
cli:
kubernetes:
delete:
integration: "route"<test name="CamelCliTest" xmlns="http://citrusframework.org/schema/xml/testcase"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://citrusframework.org/schema/xml/testcase http://citrusframework.org/schema/xml/testcase/citrus-testcase.xsd">
<actions>
<camel>
<cli>
<kubernetes>
<delete>
<integration name="route"/>
</delete>
</kubernetes>
</cli>
</camel>
</actions>
</test><spring:beans xmlns="http://www.citrusframework.org/schema/testcase"
xmlns:spring="http://www.springframework.org/schema/beans">
<!-- NOT SUPPORTED -->
</spring:beans>This deletes the Kubernetes deployment and all resources listed in the Camel integration Kubernetes manifest.
Citrus is able to verify the state of a running Camel integration in Kubernetes. This is the status of the Kubernetes deployment and its pods and containers. Also, Citrus is able to scan the log output of a Camel integration that is running in Kubernetes.
public class CamelCliTest extends TestNGCitrusSupport implements TestActionSupport {
@Test
@CitrusTest
public void shouldRunCamelCli() {
when(camel().cli()
.kubernetes()
.verify("route")
.waitForLogMessage("Hello from Apache Camel! #10"));
}
}name: "CamelCliTest"
actions:
- camel:
cli:
kubernetes:
verify:
integration: "route"
logMessage: "Hello from Apache Camel! #10"<test name="CamelCliTest" xmlns="http://citrusframework.org/schema/xml/testcase"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://citrusframework.org/schema/xml/testcase http://citrusframework.org/schema/xml/testcase/citrus-testcase.xsd">
<actions>
<camel>
<cli>
<kubernetes>
<verify integration="route" log-message="Hello from Apache Camel! #10"/>
</kubernetes>
</cli>
</camel>
</actions>
</test><spring:beans xmlns="http://www.citrusframework.org/schema/testcase"
xmlns:spring="http://www.springframework.org/schema/beans">
<!-- NOT SUPPORTED -->
</spring:beans>The action makes sure to find the Kubernetes deployment and the running Pod. The action waits for the expected log message and fails when the message is not available in the log output after the maximum number of attempts.
The Apache Camel project maintains a huge set of test infrastructure services that you can also start and stop during a Citrus test. See this documentation about the Camel test infra services.
The Camel infra services usually start Docker/Podman containers and/or use Testcontainers.
See this list of supported infrastructure services in Apache Camel:
| Service | Service | Service |
|---|---|---|
arangodb |
artemis (amqp, mqtt, persistent) |
aws (dynamodb, event-bridge, kinesis, s3, sns, sqs, sts, …) |
azure |
cassandra |
chat-script |
couchbase |
couchdb |
elasticsearch |
fhir |
ftp |
ftps |
google (pub-sub) |
hashicorp (vault) |
hazelcast |
hive-mq (sparkplug) |
kafka (confluent, redpanda, strimzi) |
microprofile (lra) |
milvus |
minio |
mongodb |
mosquitto |
nats |
ollama |
openldap |
postgres |
pulsar |
qdrant |
rabbitmq |
redis |
rocketmq |
sftp |
smb |
solr |
torch-serve |
xmpp |
zookeeper |
You can start/stop such an infra service via test actions in Citrus.
public class CamelInfraIT extends TestNGCitrusSpringSupport {
@Test
@CitrusTest
public void createInfraService() {
$(camel()
.infra()
.run()
.service("postgres"));
}
}name: "CamelInfraTest"
actions:
- camel:
infra:
run:
service: postgres<test name="CamelInfraIT"
xmlns="http://citrusframework.org/schema/xml/testcase"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://citrusframework.org/schema/xml/testcase http://citrusframework.org/schema/xml/testcase/citrus-testcase.xsd">
<actions>
<camel>
<infra>
<run service="postgres"/>
</infra>
</camel>
</actions>
</testcase><spring:beans xmlns="http://www.citrusframework.org/schema/testcase"
xmlns:spring="http://www.springframework.org/schema/beans">
<!-- NOT SUPPORTED -->
</spring:beans>Once the infrastructure service is started Citrus exposes connection settings as test variables. You can use the exposed connection settings to create proper clients that connect to the services.
The exposed connection settings follow a naming pattern that looks like this:
-
CITRUS_CAMEL_INFRA_SERVICE_<SERVICE_NAME>_<PROPERTY_NAME> -
CITRUS_CAMEL_INFRA_SERVICE_<SERVICE_NAME>_<IMPLEMENTATION>_<PROPERTY_NAME>
For instance a infrastructure service called foo that exposes the settings url and port expose the following test variables.
| Variable |
|---|
CITRUS_CAMEL_INFRA_SERVICE_FOO_URL |
CITRUS_CAMEL_INFRA_SERVICE_FOO_PORT |
You can now use these test variables when connecting to the service. Please see the individual Apache Camel infrastructure services documentation to see which properties get exposed by the service.
Some infrastructure services provide multiple implementations (e.g. Kafka provides both Redpanda and Strimzi).
You may need to choose the implementation as follows:
public class CamelInfraIT extends TestNGCitrusSpringSupport {
@Test
@CitrusTest
public void createInfraService() {
$(camel()
.infra()
.run()
.service("kafka")
.implementation("strimzi"));
}
}name: "CamelInfraTest"
actions:
- camel:
infra:
run:
service: kafka
implementation: strimzi<test name="CamelInfraIT"
xmlns="http://citrusframework.org/schema/xml/testcase"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://citrusframework.org/schema/xml/testcase http://citrusframework.org/schema/xml/testcase/citrus-testcase.xsd">
<actions>
<camel>
<infra>
<run service="kafka" implementation="strimzi"/>
</infra>
</camel>
</actions>
</testcase><spring:beans xmlns="http://www.citrusframework.org/schema/testcase"
xmlns:spring="http://www.springframework.org/schema/beans">
<!-- NOT SUPPORTED -->
</spring:beans>By default, the infrastructure services get automatically removed after the test. You can disable the auto removal when running the service (auto-remove setting).
By default, Camel infra services use random ports for better test isolation. In some cases you may want to use a fixed port instead (e.g. when an external tool expects a service on a well-known port, or when debugging locally).
You can configure a fixed port directly on the run action:
public class CamelInfraIT extends TestNGCitrusSpringSupport {
@Test
@CitrusTest
public void createInfraService() {
$(camel()
.infra()
.run()
.service("postgres")
.port(5432));
}
}name: "CamelInfraTest"
actions:
- camel:
infra:
run:
service: postgres
port: 5432Setting a specific port automatically enables fixed port mode. You can also enable fixed port mode without specifying a concrete port number — this tells the service to use its own default port rather than a random one:
$(camel()
.infra()
.run()
.service("postgres")
.fixedPort(true));name: "CamelInfraTest"
actions:
- camel:
infra:
run:
service: postgres
fixedPort: trueThe fixed port settings can also be configured globally via system properties or environment variables (see Camel infra settings).
To explicitly stop a running infrastructure service use the following test action.
public class CamelInfraIT extends TestNGCitrusSpringSupport {
@Test
@CitrusTest
public void createInfraService() {
$(camel()
.infra()
.stop()
.service("postgres"));
}
}name: "CamelInfraTest"
actions:
- camel:
infra:
stop:
service: postgres<test name="CamelInfraIT"
xmlns="http://citrusframework.org/schema/xml/testcase"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://citrusframework.org/schema/xml/testcase http://citrusframework.org/schema/xml/testcase/citrus-testcase.xsd">
<actions>
<camel>
<infra>
<stop service="postgres"/>
</infra>
</camel>
</actions>
</testcase><spring:beans xmlns="http://www.citrusframework.org/schema/testcase"
xmlns:spring="http://www.springframework.org/schema/beans">
<!-- NOT SUPPORTED -->
</spring:beans>The Camel infrastructure services support the following environment settings.
| System property | Description |
|---|---|
citrus.camel.infra.auto.remove.services |
When set to true the test will automatically stop and remove Camel infra services that have been started. (default="true") |
citrus.camel.infra.dump.service.output |
When set to true infra service output will be redirected to a file in the current working directory. (default="false") |
citrus.camel.infra.port |
When set infra services use this port as a fixed port. (default="-1", meaning random port) |
citrus.camel.infra.fixedPort |
When set to true infra services use fixed ports. Otherwise, services use random ports for better isolation. (default="false") |
citrus.camel.infra.<service>.port |
Sets a fixed port for a specific service. The service name is the full service name (e.g. |
| Environment variable | Description |
|---|---|
CITRUS_CAMEL_INFRA_AUTO_REMOVE_SERVICES |
When set to true the test will automatically stop and remove Camel infra services that have been started. (default="true") |
CITRUS_CAMEL_INFRA_DUMP_SERVICE_OUTPUT |
When set to true infra service output will be redirected to a file in the current working directory. (default="false") |
CITRUS_CAMEL_INFRA_PORT |
When set infra services use this port as a fixed port. (default="-1", meaning random port) |
CITRUS_CAMEL_INFRA_FIXED_PORT |
When set to true infra services use fixed ports. Otherwise, services use random ports for better isolation. (default="false") |
CITRUS_CAMEL_INFRA_<SERVICE>_PORT |
Sets a fixed port for a specific service. The service name uses upper case and non-alphanumeric characters are replaced with underscores (e.g. |
Since Camel 3 the endpoint DSL provides a convenient way to construct an endpoint uri. In Citrus you can use the Camel endpoint DSL to send/receive messages in a test.
$(send(camel().endpoint(seda("test")::getRawUri))
.message()
.body("Citrus rocks!"));The fluent endpoint DSL in Camel allows to build the endpoint uri.
The camel().endpoint(seda("test")::getRawUri) builds the endpoint uri seda:test.
The endpoint DSL provides all settings and properties that you can set for a Camel endpoint component.
Camel implements the concept of processors as enterprise integration pattern. A processor is able to add custom logic to a Camel route. Each processor is able to access the Camel exchange that is being processed in the current route. The processor is able to change the message content (body, headers) as well as the exchange information.
The send/receive operations in Citrus also implement the processor concept. With the Citrus Camel support you can use the very same Camel processor also in a Citrus test action.
public class CamelMessageProcessorIT extends TestNGCitrusSpringSupport {
@Autowired
private CamelContext camelContext;
@Test
@CitrusTest
public void shouldProcessMessages() {
$(send(camel().endpoint(seda("test")::getRawUri))
.message()
.body("Citrus rocks!")
.process(processor()
.camel()
.camelContext(camelContext)
.process(exchange -> exchange
.getMessage()
.setBody(exchange.getMessage().getBody(String.class).toUpperCase())))
);
}
}The example above uses a Camel processor to change the exchange and the message content before the message is sent to the endpoint. This way you can apply custom changes to the message as part of the test action.
public class CamelMessageProcessorIT extends TestNGCitrusSpringSupport {
@Autowired
private CamelContext camelContext;
@Test
@CitrusTest
public void shouldProcessMessages() {
$(send(camel().endpoint(seda("test")::getRawUri))
.message()
.body("Citrus rocks!"));
$(receive(camel().endpoint(seda("test")::getRawUri))
.process(processor()
.camel(camelContext)
.process(exchange -> exchange
.getMessage()
.setBody(exchange.getMessage().getBody(String.class).toUpperCase())))
.message()
.type(MessageType.PLAINTEXT)
.body("CITRUS ROCKS!"));
}
}The Camel processors are very powerful. In particular, you can apply transformations of multiple kind.
public class CamelTransformIT extends TestNGCitrusSpringSupport {
@Autowired
private CamelContext camelContext;
@Test
@CitrusTest
public void shouldTransformMessageReceived() {
$(send(camel().endpoint(seda("hello")::getRawUri))
.message()
.body("{\"message\": \"Citrus rocks!\"}")
);
$(receive(camel().endpoint(seda("hello")::getRawUri))
.transform(
camel()
.camelContext(camelContext)
.transform()
.jsonpath("$.message"))
.message()
.type(MessageType.PLAINTEXT)
.body("Citrus rocks!"));
}
}The transform pattern is able to change the message content before a message is received/sent in Citrus.
The example above applies a JsonPath expression as part of the message processing.
The JsonPath expression evaluates $.message on the Json payload and saves the result as new message body content.
The following message validation expects the plaintext value Citrus rocks!.
The message processor is also able to apply a complete route logic as part of the test action.
public class CamelRouteProcessorIT extends TestNGCitrusSpringSupport {
@Autowired
private CamelContext camelContext;
@Test
@CitrusTest
public void shouldProcessRoute() {
CamelRouteProcessor.Builder beforeReceive = camel(camelContext).route(route ->
route.choice()
.when(jsonpath("$.greeting[?(@.language == 'EN')]"))
.setBody(constant("Hello!"))
.when(jsonpath("$.greeting[?(@.language == 'DE')]"))
.setBody(constant("Hallo!"))
.otherwise()
.setBody(constant("Hi!")));
$(send(camel().endpoint(seda("greetings")::getRawUri))
.message()
.body("{" +
"\"greeting\": {" +
"\"language\": \"EN\"" +
"}" +
"}")
);
$(receive("camel:" + camel().endpoints().seda("greetings").getUri())
.process(beforeReceive)
.message()
.type(MessageType.PLAINTEXT)
.body("Hello!"));
}
}With the complete route logic you have the full power of Camel ready to be used in your send/receive test action. This enables many capabilities as Camel implements the enterprise integration patterns such as split, choice, enrich and many more.
Camel uses the concept of data format to transform message content in form of marshal/unmarshal operations. You can use the data formats supported in Camel in Citrus, too.
public class CamelDataFormatIT extends TestNGCitrusSpringSupport {
@Autowired
private CamelContext camelContext;
@Test
@CitrusTest
public void shouldApplyDataFormat() {
when(send(camel().endpoint(seda("data")::getRawUri))
.message()
.body("Citrus rocks!")
.transform(camel(camelContext)
.marshal()
.base64())
);
then(receive("camel:" + camel().endpoints().seda("data").getUri())
.transform(camel(camelContext)
.unmarshal()
.base64())
.transform(camel(camelContext)
.convertBodyTo(String.class))
.message()
.type(MessageType.PLAINTEXT)
.body("Citrus rocks!"));
}
}The example above uses the base64 data format provided in Camel to marshal/unmarshal the message content to/from a base64 encoded String.
Camel provides support for many data formats as you can see in the documentation on data formats.