23
votes

Let's say I have my routes created in separate RouteBuilder class. It looks like:

  • grab message from JMS queue
  • do some transformation, validation etc
  • depending on validation results forward to specific JMS queue and save something in DB

I'd like to unit test this route with no JMS broker and no DB. I know I can mock my Processor implementations but that's not enough. I don't want to change this route (let's suppose I got that class in jar file). As far as I know from Camel in Action (sec. 6.2.6), to be able to use mocks of endpoints and other stuff I need to change my route endpoint definitions (in book's example this is change of "mina:tcp://miranda" to "mock:miranda" etc).

Is it possible to test the flow in complete isolation without changing route definitions? If I got my RouteBuilder as a separate class, am I forced to somehow "copy" route definition and change it manually? Isn't it testing the wrong thing?

I'm quite new to Camel and for me it'd be really cool to be able to have isolated unit test while deveoping routes. Just to be able to change something, run small test, observe result and so on.

4

4 Answers

25
votes

Assuming the RouteBuilder class has hardcoded endpoints then its a bit tougher to test. However if the RouteBuilder using the property placeholder for endpoint uris, then you often will be able to use a different set of endpoint uris for unit tests. As explained in chapter 6 of the Camel book.

If they are hardcoded then you can use the advice with feature in your unit test as shown here: https://camel.apache.org/components/latest/others/test-cdi.html#CDITesting-RoutesadvisingwithadviceWith

In Camel 2.7 we made it possible to manipulate the route much easier, so you can remove parts, replace parts, etc. Thats the weaving stuff that link talks about.

For example to simulate sending a message to a database endpoint, you can use that above and replace the to with another where you send it to a mock instead.

In previous releases you can use the interceptSendToEndpoint trick, which is also covered in the Camel book (section 6.3.3)

Oh you can also replace components with mock component as shown on page 169. Now in Camel 2.8 onwards the mock component will no longer complain about uri parameters it doesnt know. That means its much easier to replace components with mocks on a per component level.

6
votes

I have

   <bean id="properties" class="org.apache.camel.component.properties.PropertiesComponent">
        <property name="location" value="classpath:shop.properties"/>
    </bean>

    <route>
        <from uri="direct://stock"/>
        <to uri="{{stock.out}}"/>
    </route>

in my spring file and then in the shop.properties on the test class path i have a stock.out=xxxx which is replaced at runtime so i can have to different routes one for runtime and one for test

theres a better example in 6.1.6 unit testing in multiple environments

4
votes

While you can use intercepts and advice to swap out endpoints as per Claus Ibsen's answer, I think that it is far better to allow your routes to accept Endpoint instances so that your tests aren't coupled to your production endpoint URIs.

For example, say you have a RouteBuilder that looks something like

public class MyRoute extends RouteBuilder {
    @Override
    public void configure() throws Exception {
        from("http://someapi/someresource")
        .process(exchange -> {
            // Do stuff with exchange
        })
        .to("activemq:somequeue");
    }
}

You can make it possible to inject endpoints like so:

public class MyRoute extends RouteBuilder {
    private Endpoint in;
    private Endpoint out;

    // This is the constructor your production code can call
    public MyRoute(CamelContext context) {
        this.in = context.getEndpoint("http://someapi/someresource");
        this.out = context.getEndpoint("activemq:somequeue");
    }

    // This is the constructor your test can call, although it would be fine
    // to use in production too
    public MyRoute(Endpoint in, Endpoint out) {
        this.in = in;
        this.out = out;
    }

    @Override
    public void configure() throws Exception {
        from(this.in)
        .process(exchange -> {
            // Do stuff with exchange
        })
        .to(this.out);
    }
}

Which can then be tested like this:

public class MyRouteTest {
    private Endpoint in;
    private MockEndpoint out;
    private ProducerTemplate producer;

    @Before
    public void setup() {
        CamelContext context = new DefaultCamelContext();

        this.in = context.getEndpoint("direct:in");
        this.out = context.getEndpoint("mock:direct:out", MockEndpoint.class);
        this.producer = context.createProducerTemplate();
        this.producer.setDefaultEndpoint(this.in);

        RouteBuilder myRoute = new MyRoute(this.in, this.out);
        context.addRoutes(myRoute);

        context.start();
    }

    @Test
    public void test() throws Exception {
        this.producer.sendBody("Hello, world!");
        this.out.expectedMessageCount(1);
        this.out.assertIsSatisfied();
    }
} 

This has the following advantages:

  • your test is very simple and easy to understand, and doesn't even need to extend CamelTestSupport or other helper classes
  • the CamelContext is created by hand so you can be sure that only the route under test is created
  • the test doesn't care about the production route URIs
  • you still have the convenience of hard-coding the endpoint URIs into the route class if you want
0
votes
  1. In case you are using Spring (which is mostly a good idea), I'd like to share my approach.

    Your production route is a Spring bean with it's special class MyRoute

    @Component
    public class MyRoute extends RouteBuilder {
    
        public static final String IN = "jms://inqueue";
    
        @Override
        public void configure() throws Exception {
            from(IN)
            .process(exchange -> {
                // Do stuff with exchange
            })
            .to("activemq:somequeue");
        }
    }
    

    So in the test you can easily override it like this (this is a spring java config internal (to the test class) class):

    static class TestConfig extends IntegrationTestConfig {
    
            @Bean
            public MyRoute myRoute(){
                return new MyRoute() {
                    @Override
                    public void configure() throws Exception {
                        interceptFrom(MyRoute.IN)
                                .choice() 
    
                                    .when(x -> delayThisMessagePredicate.matches(x)) //make the predicate modifiable between tests
                                    .to("log:delayed")
                                    .delay(5000)
                                .endChoice();
                        super.configure();
                    }
                };
            }
    }
    

    Notice super.configure() installs your production route and you may use interceptFrom, interceptSendToEndpoint to inject test code: e.g. raise an exception.

    I also add some helper routes. With this route I can test, that a file has been generated in an output folder, it may be a JMS consumer...

        @Bean
        public RouteBuilder createOutputRoute() {
    
            return new RouteBuilder() {
                @Override
                public void configure() {
    
                    fromF(FILE_IN,
                            outputDir)
                            .to("mock:output")
                            .routeId("doneRoute");
            };
    
  2. For JMS/JDBC/... there is also Mockrunner. With the code below in your test config you are almost done: your JMS Connection Factory is replaced by a mock implementation, so now you can even put something to JMS and read from jms (using simple camel route like explained above) to verify. Don't forget to create a queue on the mock.

    @Bean(JMS_MOCK_CONNECTION_FACTORY) @Primary public ConnectionFactory jmsConnectionFactory() { return (new JMSMockObjectFactory()).getMockQueueConnectionFactory(); }

  3. I don't like AdviseWith, yes it's flexible, but it requires you in the test to manually handle camelContext, which is too intrusive for me. I don't want to put that code in hundreds of tests and I also don't want to create a framework around it. Also subclassing CamelTestSupport can be a problem for example if you would use two libraries, which both require you to subclass something and you might have your own test class hierarchy, where you don't see CamelTestSupport. I try not to have class hierarchy in my tests to leave the tests independent. Subclassing means, that you require some magic to happen (you don't directly see that code in the test). If you modify that magic, you would affect a lot of tests. I use spring java config sets for that.