October 20, 2015

Testing Apache Camel Applications with Spring Boot

In a previous blog post my colleague Rene Schakmann has shown that Apache Camel is a versatile tool that integrates different components using well-known enterprise integration patterns. At willhaben, we use Apache Camel together with Spring Boot for many different projects. However, writing unit tests for these Camel routes is not that trivial and requires some knowledge of both frameworks. In this blog post, I will walk you through a sample project (available on github) with special regard given to testability.

Setup and configuration

The project is a pretty standard Java Maven project. In order to be able to properly write unit test, we need to use some dependencies (see pom.xml). Apart from the usual Camel and Spring Boot dependencies, the most important ones are:

<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-spring-boot</artifactId>
    <version>${camel.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>${spring.boot.version}</version>
    <scope>test</scope>
</dependency> 

Apache Camel provides a module which integrates nicely into Spring Boot: camel-spring-boot. With this module, we can take full advantage of spring boot features (like autoconfiguration, bulding fat-jars, injecting properties, profiles, etc.). The spring-boot-startet-test dependency helps us with setting up our test cases, which we will see later.

Implementation

Our test project implements one simple Camel route which monitors a folder for new files. So, for example, when the file name contains "cat", the file is moved to the "cats" folder, and when it contains "dog" it is moved to the "dogs" folder.

To implement this, we use the built-in file component, which uses the URL prefix "file://". However, to test our business logic we will have to change the endpoint URLs to send test messages via the route directly (instead of consuming from the file endpoint) and verify the output with mock endpoints (and not moving the files), so our AnimalRoute class looks like this:


@Component
 public class AnimalRoute extends RouteBuilder {

    public static final String CAMEL_FILE_NAME = "CamelFileName";

    @Override 
    public void configure() throws Exception {

        from("{{animalSource}}")
                .log("got message")
                .choice()
                .when(p -> p.getIn().getHeader(CAMEL_FILE_NAME).toString()
.contains("dog"))
                    .log("found a dog!")
                    .to("{{dogEndpoint}}")
                .when(p -> p.getIn().getHeader(CAMEL_FILE_NAME).toString()
.contains("cat"))
                    .log("looks like a cat!")
                    .to("{{catEndpoint}}");
    }
}

Our component is pretty minimal and easy to read. Endpoints with double parentheses like "{{animalSource}}" will get injected with values from property files. We will use Spring profiles to change endpoint URLs for development, production and unit testing.

Apart from this route the application consists of just one more class: Main. It serves as an entry point for our application. The only speciality in this class is that we tell spring boot to wait for "Ctrl+C" to stop the application:

@EnableAutoConfiguration
@ComponentScan 
public class Main {

    public static void main(String[] args) throws Exception {
        ConfigurableApplicationContext configurableApplicationContext = 
SpringApplication.run(Main.class, args);
        CamelSpringBootApplicationController configurableApplicationContextBean = 
configurableApplicationContext.getBean(CamelSpringBootApplicationController.class);
        configurableApplicationContextBean.blockMainThread();
    }
}

Writing Tests

Our unit test will have to do several things:
  • create  the context, 
  • load the correct properties for testing, 
  • inject messages into the route and 
  • verify the result messages. 
 Luckily we can to this with our setup in one single class only using annotations:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = AnimalRouteTest.class)
@ActiveProfiles("test")
@EnableAutoConfiguration 
@ComponentScan 
public class AnimalRouteTest extends TestCase {

    public static final String NICE_DOG = "nice dog", NASTY_CAT="nasty cat", SUPERNASTY_CAT="super nasty cat";

    @EndpointInject(uri = "{{dogEndpoint}}")
    protected MockEndpoint dogEndpoint;

    @EndpointInject(uri = "{{catEndpoint}}")
    protected MockEndpoint catEndpoint;

    @EndpointInject(uri = "{{animalSource}}")
    protected ProducerTemplate animalSource;

    @Test 
    @DirtiesContext 
    public void testDog() throws Exception {

        animalSource.sendBodyAndHeader("test",AnimalRoute.CAMEL_FILE_NAME,NICE_DOG);

        dogEndpoint.expectedMessageCount(1);
        dogEndpoint.message(0).predicate(m -> {
            String header = m.getIn().getHeader(AnimalRoute.CAMEL_FILE_NAME).toString();
            return NICE_DOG.equals(header);
        });
        dogEndpoint.assertIsSatisfied();

        catEndpoint.expectedMessageCount(0);
        catEndpoint.assertIsSatisfied();
    }
}

For our tests we want to have the producer endpoint and both consumer endpoints, and we get all of this using the @EndpointInject annotation (note that we again use the curly brackets notation from before). In the test itself we can now easily send a test message, and use the assertion methods of the Camel testing framework to check if the test message got to the right endpoint. The @DirtiesContext annotation ensures that we always get a fresh Camel context for each test.

Now, the only missing pieces are the property files for our environments. With spring profiles, we just have to name our files application-{env}.properties where {env} is the profile which should use this configuration:

animalSource=direct:animalSource 
dogEndpoint=mock:dogEndpoint 
catEndpoint=mock:catEndpoint

for application-test.properties

animalSource=file://animals 
dogEndpoint=file://dogs 
catEndpoint=file://cats

for application-dev.properties, application-prod.properties, etc...

Conclusion

Although the result is pretty straightforward, understanding all parts and setting up everything can take quite some time. I hope that this post has helped you to gain some insight and will help you get started easily or even start testing right away.

8 comments:

  1. The biggest problem with testing camel routes is their async nature. That is, the test reaches its asserts before the message has passed through the route, making the test fail. Then you need to use waits in the asserts, which makes the test slower.

    Would have been nice if the waits at least could be like Spock's PollingConditions, where you can specify that the assert is repeatedly tested every x seconds before giving up after y seconds. Then you would not have to wait the full time (y seconds) when it actually was ready after x seconds.

    ReplyDelete
    Replies
    1. yes, thats true, but camel does provide some testing capabilities for async testing, for example https://camel.apache.org/maven/camel-2.15.0/camel-core/apidocs/org/apache/camel/component/mock/MockEndpoint.html#setAssertPeriod%28long%29

      if you add "dogEndpoint.setAssertPeriod(1000);" in the testDog() test, it will reevaluate after 1000 milis if it is still valid.

      Delete
    2. Don't get me wrong, I think camel is great; we use it extensively at FINN Reise (http://www.finn.no/reise).

      Did not know about the setAssertPeriod(...). That partly solves the hardest problem when testing async code: knowing that nothing would have happened after the test is finished.

      Regarding Spock's PollingConditions; here is a gist with how it can be done: https://gist.github.com/stigkj/eb794067e41fe11aca2e

      Line
      3: sends a message into the route
      5-8: sets up a PollingCondition that will check that the assert in its closure will go true before exiting. It has a timeout of 10 seconds, waits 1.5 second before checking the assert the first time, and grows the delay by 1.25 seconds between subsequent checks.
      9: asserts that the received message includes a string

      Here solr is a mock for a Solr client that is called from the route with processed messages. That is, solr.received is a list of received messages.

      Delete
  2. This doesn't seem to work with camel 2.17.0 and spring boot 1.3.3.RELEASE. Any idea what could be causing the issue? It appears that the properties injection no longer works.

    ReplyDelete
    Replies
    1. sorry about that, it should work now. the error was that the properties file was only available for the dev profile.

      Delete
  3. Hi Matthias, thank you for the great post. Thanks to it I solved a problem with mocking endpoints (I used camel-spring instead of camel-spring-boot in maven dependencies).

    ReplyDelete
  4. Java is the best object oriented programming language

    ReplyDelete
  5. Setting up everything can take quite some time. I don't like

    ReplyDelete