Testing the messaging applications (Advanced)
Integration applications are asynchronous, heterogeneous, and message driven in nature. Traditionally testing such applications is challenging, especially when there is not good tooling support. As a consequence, most of the testing is done manually at the end of the project with or without very little automated tests. Fortunately, Camel offers a variety of helper tools and makes writing routing tests a pleasurable activity. It can run routes isolated in a test container, mock external systems, specify expectations, trigger events, match expectations, simulate load or certain behavior, and so on. Let's see how to test a route written in Java DSL and then the additional tools and techniques used with Camel.
Getting ready
The complete source code for this tutorial is located under the following project: camel-message-routing-examples/testing-routes
.
We will use JUnit
, but there is also TestNG
support, although it has fewer features.
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit-version}</version> <scope>test</scope> </dependency>
For testing applications using Java DSL we need the camel-test
dependency:
<dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-test</artifactId> <version>${camel-version}</version> <scope>test</scope> </dependency>
How to do it...
Let's assume that we want to test the following route defined in Java DSL:
public class SimpleChoiceRoute extends RouteBuilder { @Override public void configure() throws Exception { from("direct:start") .choice() .when(body().isEqualTo("orange")) .to("mock:oranges") .when(body().isEqualTo("apple")) .to("mock:apples");} }
To test this route we have to extend the abstract
CamelTestSupport
class in our test and instantiate the route we want to test in thecreateRouteBuilder
method:public class SimpleChoiceRouteTest extends CamelTestSupport { @Override protected RouteBuilder createRouteBuilder() throws Exception { return new SimpleChoiceRoute(); } }
For each test,
CamelTestSupport
will create aCamelContext
, run our route under test, and then tear everything down at the end of the test. Let's add our first test:@Test public void sendsAnAppleMessage() throws Exception { MockEndpoint mockOranges = getMockEndpoint("mock:oranges"); MockEndpoint mockApples = getMockEndpoint("mock:apples"); mockOranges.setExpectedMessageCount(0); mockApples.setExpectedMessageCount(1); template.sendBody("direct:start", "apple"); mockOranges.assertIsSatisfied(); mockApples.assertIsSatisfied(); }
The test method first gets hold of the mock endpoint and sets the expected number of messages, then using
producerTemplate
triggers an event and finally verifies that the expectation is matched. This is the generalexpect-run-verify
test pattern. To make the test more readable, we could do a little bit of refactoring. For example, use annotations and declareMockEndpoints
as instance variables, specify defaultEndpoint
forProducerTemplate
, assert all mock endpoints in once step, and so on:@Produce(uri = "direct:start") protected ProducerTemplate start; @EndpointInject(uri = "mock:oranges") private MockEndpoint mockOranges; @EndpointInject(uri = "mock:apples") private MockEndpoint mockApples; @Test public void orderSomeFruits() throws Exception { mockOranges.expectedBodiesReceived("orange"); mockApples.expectedBodiesReceived("apple"); start.sendBody("orange"); start.sendBody("apple"); assertMockEndpointsSatisfied(); }
How it works...
CamelTestSupport
is the base class for testing routes. During the setup stage, it will create a new CamelContext
, producerTemplate
, consumerTemplate
, load the routes defined in the test class and start them. Then, in our test, we can access mock endpoints, set expectations and trigger events or directly send messages to the route endpoints using producerTemplate
. At the end of each test method CamelTestSupport
will stop the routes and CamelContext
(unless it is configured to tear down everything at the end of all tests in the test class).
The base class also provides lots of other helpful methods and hooks to customize the test lifecycle. For example, in some cases it is required to set expectations or do some work before starting the routes. That is possible by overriding the isUseAdviceWith
method to return true
which will prevent CamelContext
from starting automatically and it has to be started manually as part of tests:
@Override public boolean isUseAdviceWith() { return true; } public void pollsFilesOnStart() throws Exception { getMockEndpoint("mock:result").expectedBodiesReceived("Some file content"); camelContext.start(); assertMockEndpointsSatisfied(); }
Mock endpoint (http://camel.apache.org/mock.html) is another helpful tool used for testing routes. It is similar to other mocking libraries such as Mockito and jMock, and represents an inmemory list that collects all Exchanges
it interacts with. It has mainly two types of methods: for specifying expectations and for verifying them, but it can also simulate specific behavior:
mockReplying.whenAnyExchangeReceived(new Processor() { @Override public void process(Exchange exchange) throws Exception { Message in = exchange.getIn(); in.setBody("Mock response: " + in.getBody()); } });
The previous code snippet configures mockReplying
to modify the message body when an Exchange
is received in order to simulate the behavior of the mocked endpoint.
There's more...
Testing Spring XML DSL is no different, apart from using the Spring's ApplicationContext
for creating the routes. Here, we will have a look at that and a few other testing tools.
Testing applications written in Spring XML DSL
To test Camel applications written in XML DSL, the camel-test-spring dependency is needed.
<dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-test-spring</artifactId> <version>${camel-version}</version> <scope>test</scope> </dependency>
The main difference to testing Java routes is that, instead of extending CamelTestSupport
, the test class has to extend CamelSpringTestSupport
and override createApplicationContext
, rather than createRouteBuilder
:
public class SimpleChoiceRouteSpringTest extends CamelSpringTestSupport { @Override protected AbstractApplicationContext createApplicationContext() { return new ClassPathXmlApplicationContext("META-INF/spring/simple-choice-route-context.xml"); } //the rest of the code is the same as previous }
CamelSpringTestSupport
provides feature-parity with CamelTestSupport
, which means it allows tests to be written in the same style, provides the same protected variables, methods, and honors the same lifecycle. In fact we can use the same instance variables and test method from the previous example to test the Spring XML route.
There is also a way for running tests without extending the CamelSpringTestSupport
class, called Enhanced Spring Test Support. In this scenario, the test class has to be annotated with @RunWith
(CamelSpringJUnit4ClassRunner.class
), and use other annotation to inject Endpoints, CamelContext, and so on, because they are not available as protected fields of the parent class:
@RunWith(CamelSpringJUnit4ClassRunner.class) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) @ContextConfiguration(locations = {"classpath:/META-INF/spring/simple-choice-route-context.xml"}) public class SimpleChoiceRouteEnhancedTest { @Autowired private CamelContext camelContext; @Produce(uri = "direct:start") protected ProducerTemplate start; //the rest of the class is the same as the example previous }
The previous tests are mainly testing the message transformations and the routing logic in Camel routes. If the application interacts with external systems, there is also a need for integration testing to verify the interaction points. To see more testing techniques check the tests for the other examples from this book.
Other tools for testing
AdviceWith (http://camel.apache.org/advicewith.html): Sometimes, a route has hardcoded Endpoints which makes it hard to test, or it might be necessary to modify part of the route before testing. For these kinds of scenarios Camel provides an Aspect-Oriented-Programing (AOP) model with the name
AdviceWith
. This feature makes it possible to modify the routes after they are loaded toCamelContext
and provides methods to add, remove, and replace endpoints by name or ID.NotifyBuilder (http://camel.apache.org/notifybuilder.html): This allows testing routes without modifying them by building conditional expressions and then testing or waiting for that condition to occur. Good for integration testing with external endpoints.
DataSet (http://camel.apache.org/dataset.html): The DataSet component provides a mechanism for easily performing load and soak testing of the system.