Tom Donohue Tom Donohue

The Guide to Testing in Apache Camel

How to use Camel’s test framework to test your routes

The Guide to Testing in Apache Camel

Tags: Comments

Today I want to talk about testing. (Crowd runs away screaming…)

Hey, come back!! Testing isn’t actually that hard. You can actually unit test your Camel routes with relative ease, once you know about the Camel Test Kit.

The Camel Test Kit is a set of extensions and APIs which allow you to test your Camel applications. They make it easier to test your Camel routes by sending messages, and checking that messages are received as expected. Find the test Kit in Camel’s camel-test dependency.

Writing tests can be rather fun! Testing a Camel route is like turning on a sausage machine…. and then verifying that it’s actually making sausages:

Camel testing is like inspecting a sausage machine

Yummy sausages. 🌭

In this article, we’ll look at the main concepts you need to know when testing Camel, how to set up unit tests for your sausage machine Camel routes, see some of the testing APIs you can use, and some advice on how to write good tests.

How to test Camel routes

To test a Camel route, you need to start with the Camel Test Kit. The Camel Test Kit is a set of extensions for JUnit, helper classes and APIs, which allow you to control your routes, send test messages, perform assertions and more.

The test kit is found in the camel-test and camel-test-spring dependencies.

Step 1: Bootstrap Camel and add your routes

Before we can run a test, we need to start the Camel Context, and add the stuff we want to test (routes and components).

To help you do this, Camel comes with a few abstract test helper classes. They are there for you to subclass (extend), add your routes, and then add your tests.

These helper classes manage the lifecycle of Camel during the test. You supply your routes by overriding one of the methods. The classes also give you objects for working with the camel context (CamelContext) and for sending messages to Camel (ProducerTemplate):

The two main helper classes you’ll probably use are:

  • org.apache.camel.test.junit4.CamelTestSupport - a class for testing a Camel application where you have your routes in a RouteBuilder. You supply your RouteBuilder by overriding the createRouteBuilder() method.

  • org.apache.camel.test.spring.CamelSpringTestSupport - a class for testing a Camel application which uses the Spring XML DSL. You supply your XML DSL file by overriding the createApplicationContext() method.

By using one of these classes, you’ll get:

  • A Camel Context started up before you run your tests, with a method to override to supply your RouteBuilder classes

  • Graceful shutdown of your Camel Context after your tests

  • A ProducerTemplate object which you can use to send messages to Camel

  • The CamelContext object which you can use to modify your routes

  • Various other helper methods you can use to make changes to Camel during the test

Step 2: Add Mock components

Mock components are like dummy components in Camel. The purpose of a mock component is to allow you to test how Camel behaves, without having to hit a “real” external system.

Mock components are like any other component in Camel, except they don’t do anything. They are designed to just pass messages on.

You can also add assertions and behaviours to mock components. Assertions allow you to test whether the mock component received a certain message. Behaviours allow you configure a mock component so that it returns specific messages under certain conditions. If you want a mock component to return the kind of message that a real system would return, then you can configure this as a behaviour.

Diagram of a Camel route with a Direct component, followed by a Bean, and finally a Mock. Text reads: 'Mock component receives a message, and performs assertions on it'
A typical Camel route with a mock component

For example: Let’s say that you have written a Camel route which receives a message from an ActiveMQ queue and then sends the data in the message to Salesforce. When you write a unit test, you don’t want to hit the “real” Salesforce, so you would replace Camel’s salesforce: component with a mock. This allows you to test the behaviour of Camel in isolation.

How do you add Mock components to a Camel route? You can try one of the following approaches:

  • Modify routes during the test, using Camel’s AdviceWithRouteBuilder class, or Camel’s mockEndpointsAndSkip method - which can modify and replace real components with mocks

  • Make your endpoint URIs configurable - Use placeholders in your routes like .to("{{my.endpoint}}"), instead of endpoint URIs. When you run your tests, you can set these properties to mock: endpoint URIs

  • Specify a mock component URI like mock:foo in your Camel routes explicitly - although this would mean hard-coding a bunch of mock: endpoints in your routes, which isn’t practical.

Step 3: Add the test case

Now we add some test cases.

Each test case is a method in JUnit. So we create a method and annotate it with @Test.

Camel test cases usually follow this approach:

  1. Arrange – we configure our mock endpoints. We make some assertions, like “The mock component will receive 1 message”, or “The mock component will receive the message ‘Hello’, 3 times”.

  2. Act – we send a message to the route. This is our way of triggering the route. We choose a message that we want to send to the route, as part of the test. This might be a typical message that the route would normally receive, or perhaps it would be an edge case, or an invalid message, to test whether it causes the app to crash.

  3. Assert – we assert that our mock endpoints have been satisfied. This means that we check that the assertions that we set up in the first step, have proved to be true. Usually, this means that we verify that a mock endpoint received the message we expected.

An example Camel route test (Java DSL)

Based on the steps above, let’s see an example, implemented for an application that uses Camel’s Java DSL.

First, here’s the RouteBuilder class which would normally be passed to Camel, to add routes to the context. Here, the route starts with a Direct component, followed by processing with a bean, and finally sending to a mock: endpoint:

import org.apache.camel.builder.RouteBuilder;

public class MyRouteBuilder extends RouteBuilder {

    @Override
    public void configure() throws Exception {
        from("direct:start")
                .bean(MyBean.class)
                .to("mock:output"); // Defines a Mock endpoint called 'output'
    }
}

To write a test, we create a test class which extends CamelTestSupport. In the class, we override createRouteBuilder to return MyRouteBuilder, which contains the routes to be tested.

We also add a test case, in the form: Arrange, Act, Assert. It will set up the mock component assertions, it will send a message to a route, and then assert that the mock endpoints were satisfied. We use the template object which is a ProducerTemplate, provided by the abstract class:

import org.apache.camel.test.junit4.CamelTestSupport;
import org.apache.camel.RoutesBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.junit.Test;

public class MyQuickCamelTest extends CamelTestSupport {

    // We override this method with the routes to be tested
    @Override
    protected RoutesBuilder createRouteBuilder() throws Exception {
        // We override this method and provide our RouteBuilder class
        return new MyRouteBuilder();
    }

    // We write a simple JUnit test case
    @Test
    public void testRoute() throws Exception {
        // 1. Arrange.. get the mock endpoint and set an assertion
        MockEndpoint mock = getMockEndpoint("mock:output");
        mockOutput.expectedMessageCount(1);

        // 2. Act.. send a message to the start component
        template.sendBody("direct:start", null);

        // 3. Assert.. verify that the mock component received 1 message
        assertMockEndpointsSatisfied();
    }

}

There you have a simple test! We get the mock endpoint, assert that it will receive 1 message, send a message to the route, and verify that our assertions were correct.

That’s the simplest example of testing a route in Apache Camel, with a mock component.

Different types of testing in Apache Camel

You’re probably keen to start writing a test, but you might want to think about which type of testing you want to do.

Camel is an integration framework. This means it’s designed for interacting with many different systems and applications. Do you want to unit test your routes in isolation, and simulate real interactions? Or do you want to integrate with real systems and components?

Unit testing

Unit tests test a small unit of business logic in isolation. A unit test shouldn’t communicate with external systems (like message brokers or databases). It should just test the code.

Most people start with unit tests and then work upwards. It’s almost always easier to write unit tests, than setting up something more complex like integration tests or system tests.

In Camel, you can use Mock endpoints to achieve this. They allow you to test your route logic without having to stand up the external APIs or components which your route depends on. Mock endpoints do this by replacing the real components in your routes with fakes, or mocks.

Integration and system testing

The author and software architect Martin Fowler gives this definition of integration testing:

Integration tests determine if independently developed units of software work correctly when they are connected to each other.

With Camel, integration testing is a bit of a tricky concept to map. By “independently developed units of software”, do we mean Camel routes, which can be tested in isolation? Or do we mean testing a whole Camel context? And what are the other “units of software” that we’re integrating with?

Personally, I think of Integration tests and system tests as testing how different components integrate with each other. This could include testing against a real database (or a test instance of one), or a real message broker. I also think about this as testing “across the boundaries” of your application.

As an example, I would consider both of these to be integration tests:

  • A test that verifies we can make an HTTP REST call to our Camel route, and Camel returns a ‘200’ status code; because we are hitting the HTTP interface directly, perhaps using an HTTP client.

  • A test that triggers some Camel code which sends a message to an ActiveMQ queue, and then inspecting the queue on a real broker, to verify that the message arrived

Comparing integration tests and unit tests in Camel

The following table compares the differences between implementing unit tests and integration tests for an Apache Camel based application:

  Unit testing Integration/system testing
What is tested? Test one Camel route only Test the application as a whole
Use of Camel components Replace components with mocks Use live, real components
Connections to external systems No connections to external systems - just mocks Connect to test instances, e.g. a test message broker
Purposes of tests Test business logic; test for bugs (regression); test for edge or unusual cases Test that the component integrates successfully with another component
Using Spring in tests? Mostly not necessary Yes, if you use Spring
Third-party systems needed? None Real, or test instances of the systems you’re connecting to
Frameworks and technologies used JUnit + Camel Test Kit JUnit + Camel Test Kit, and optionally Testcontainers, Arquillian, RestAssured, Spring Boot Test, Spring’s RestTemplate…
Example test Send a message to a Camel route using the Camel API (e.g. invoke a Direct component), then assert that a Camel mock component receives an expected message Invoke a route by its public-facing API (e.g. a REST API, or directory where it receives files), and/or assert that a third-party system receives the expected message
Number of tests Usually higher; a bigger number of quick, simple tests Usually fewer; a smaller number of more complex “end-to-end” tests
Relative difficulty to implement Easier Harder

In reality, the line between unit testing and integration is not always so clean-cut.

For example: you might use Spring Boot’s test support, which bootstraps all your Spring beans to allow you to test your application as a complete whole. And then within your tests themselves, you can still use the Camel Test Kit to modify your routes, or replace components with mocks.

How to write great unit tests

Here’s a brief refresher on writing good unit tests.

When you write unit tests, you want to make sure they’re useful to you and the future maintainers of the code that you write today.

I always try to remember that:

  • Unit tests should test a contract – in Camel terms, that means testing everything in-between the “edges” of your routes. This means you should test the logic of your Camel routes.

  • Unit tests should run in isolation – this means that the outcome of one test shouldn’t affect another.

    Or, to put it another way: running your tests in a different order shouldn’t have any impact on their success.

  • Unit tests should be maintained as your code evolves – once you write a test, make sure you keep it maintained as your code grows.

    That means don’t be tempted to use -DskipTests=true! It’s naughty!

  • Write unit tests to help future developers find out how your code works - Unit tests can be so much better than documentation in describing how a piece of code works. So make the effort to write sensible, readable tests.

With those principles in mind, let’s have a look at how to set up tests for your Camel apps.

Other types of Camel tests

Camel on Karaf (Blueprint) testing

If you’re using the Blueprint dependency injection framework – because you’re deploying into an OSGi container like Apache ServiceMix – then you can use Camel’s Blueprint test support.

First, you need to add camel-test-blueprint as a dependency:

<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-test-blueprint</artifactId>
    <version>${camel.version}</version>
    <scope>test</scope>
</dependency>

Then, you need to write a test class which extends CamelBlueprintTestSupport.

In your test class, you’ll need to override the method getBlueprintDescriptor. Return the path to your Blueprint XML definition that defines your Camel Context. This tells Camel where to find your Blueprint file, so it can start the Camel Context.

Here’s an example to get you started:

import org.apache.camel.test.blueprint.CamelBlueprintTestSupport;

public class MyCamelAppTest extends CamelBlueprintTestSupport {

    @Override
    protected String getBlueprintDescriptor() {
        return "/OSGI-INF/blueprint/camel-context.xml";
    }

    @Test
    public void testContextOk() throws Exception {
        // A very simple test...
        // We test that the Camel Context starts correctly. (so our XML is valid!)
        // `context` is the injected CamelContext, provided by CamelBlueprintTestSupport
        assertTrue(context.getStatus().isStarted());
    }  

}

If you also want to inject some configuration properties because you’re using the OSGi Config Admin service, then override the loadConfigAdminConfigurationFile method. This tells Camel Blueprint Test where your test properties are located:

@Override
protected String[] loadConfigAdminConfigurationFile() {
    return new String[] {
        "src/test/resources/etc/properties.cfg",  // path to your file
        "my-config-pid" // the config admin 'PID' name
    };
}

Camel testing essential APIs

Now you’ve set up your skeleton test class, I’m going to run through some of the features of the Camel Test Framework which you can use in your tests.

Using Mock endpoints

First identify a MockEndpoint that you will be observing. Then, set some assertions on that endpoint – like, how many messages you expect to arrive there, or what the content of the messages should be:

MockEndpoint mock = getMockEndpoint("mock:output");

// Assert that 1 message will be received
mock.expectedMessageCount(1);

// Assert that 1 message will be received, with the body 'Hiya!' (oh hiya!)
mock.expectedBodiesReceived("Hiya");

Then send a message to an endpoint…:

// 'template' is a ProducerTemplate, provided by CamelTestSupport
template.sendBody("direct:start", "Hiya");

End your test case with an assertion, like one of these:

// Assert the endpoint is satisfied within 5 seconds
assertIsSatisfied(5L, TimeUnit.SECONDS, mock);

Sending data to endpoints

Sending data to an endpoint from a file:

template.sendBody("direct:start", new File("src/test/data/chickens.xml"));

Camel testing best practices

I thought it was worth sharing my own personal best practices. These are the practices I use when writing tests for Camel:

  • Divide complex routes down into smaller ones, and join them together, using Direct endpoints or similar glue. This makes it easier for you to test just your business logic.

    For example - with the routes below, we can just test the processCustomer route in isolation, without having to worry about the web service call, or the File component:

    from("direct:start")
        .split()
        .to("direct:processCustomer") // Call another route which contains some logic
        .to("http://example.com/webservice")
        .to("file:customers/processed");
    
    // We can more easily test this route because
    // it just contains logic, and no interactions with other systems
    from("direct:processCustomer")
        .bean(ProcessCustomer.class)
        .bean(SaveCustomer.class);
    
  • Replace endpoints with mocks - mock endpoints in Camel really give you superpowers. They come with all sorts of methods to test whether a message has arrived, whether it matches certain criteria, whether messages have arrived in the right order… You could write all these tests yourself, or you could just use mocks. Seriously, just use mocks.

    This quick example shows what’s possible using just a small portion of the API. It’s easy to read, and very quick to write:

    MockEndpoint mock = getMockEndpoint("mock:result");
    mock.expectedMessageCount(1);
    mock.expectedBodiesReceived("Hello, world!");
    
    // Verify that the right message arrived within 60 seconds
    assertMockEndpointsSatisfied(60, TimeUnit.SECONDS);
    

    And how do you put mocks in your routes without rewriting your actual route code? Well, there’s an answer to that…

  • Make your endpoints configurable - instead of hard-coding endpoint URIs in your routes, use placeholders. This gives you so much more flexibility to be able to replace your endpoints with anything you like at testing time. This is an ideal way of throwing mock endpoints into your routes.

    If you’re using Spring Boot, this is very easy. For example, in the route below, I’ve made the from and to endpoints as placeholders:

    from("{{uri.from}}")
        .bean(MyCustomBean.class)
        .to("{{uri.to}}");
    

    Then, in your test class, you can replace these with whatever endpoints you like:

    @SpringBootTest(classes = YourApplication.class, properties = {
        "uri.from = direct:start",
        "uri.to   = mock:output" })
    
  • Don’t write tests for Camel functionality - for example, don’t write a test which checks whether Camel can write a file, or whether it can connect to ActiveMQ. Camel’s already tested all of that in its source code!

    Instead, it’s better to focus on testing your business logic and your code. For example, you should test whether a customer is flagged correctly based on their loyalty status, or that your order validation works.

  • Write an additional test whenever you fix a bug or find an edge case - use bug fixing as an opportunity to write a new test, so that the same bug cannot be reintroduced.

    @Test
    public void testCustomerWithNoAddressIsRejected() {
      //...write stuff here
    }
    

So there you have it. You can test your routes in Apache Camel with mock endpoints and assertions.

Have you got your own approach to testing Camel routes? Share it in the comments!

Sausage machine photo by Wim Klerkx, CC-BY-NC-CD   Doctor's surgery and Camel illustration by pch.vector on Freepik  

Comments

What do you think? You can use Markdown in your comment. To write code, indent each line with 4 spaces.