Unit Testing Camel Routes with Spock

Unit testing Camel Routes with the Spock testing framework in a Spring Boot application..

Chris Tosspon

# Introduction

Apache Camel’s variety of components and message routing logic capabilities makes testing a requirement to ensure your routes are performing as expected. Since Camel routes are built within a context, it usually follows that to test those routes an integration test is required to verify the routing logic.

But what if you don’t want a full fledged context just to have a test case to cover some simple routing logic? Do you really need to wire up a test database and provide all the other information an integration test requires just to test a conditional in your routing logic? Of course not, unit testing would be the obvious and logical choice.

To that end, I’ve chosen my favorite testing framework, Spock, to explore a simple way to unit test camel routing logic.

# Camel Route Builder

Let’s say you have a simple route builder class that takes in a message object, performs a simple operation on that message and passes it along to an output route that spits it out to a pre-defined log.

@Component
class SimpleRouter extends RouteBuilder{

    @Autowired
    SimpleInputService simpleInputService

    @Autowired
    SimpleOutputService simpleOutputService

    @Override
    void configure() throws Exception {

        from('direct:test-input')
            .log(DEBUG, log,'Received message on test-input')
            .bean(simpleInputService)
            .choice()
            .when(header('SEND_OUT').isNotNull())
                .log(DEBUG, log,'Message is valid and will be sent to direct:test-output')
                .to('direct:test-output')
            .endChoice()


        from('direct:test-output')
            .log(DEBUG, log, 'Received message on test-output')
            .bean(simpleOutputService)
            .to('log:out')
    }

}

# Unit Test

## Setup

Let’s take a look at what’s needed to unit test the “choice” logic in this camel route.

First, start with some basic Spock mocking and test setup. For this specific class, we see two services that are autowired and used in the route logic. Let’s mock those out as we don’t care to test those specific services functions, just that they were called at the appropriate times.

def mockSimpleInputService = Mock(SimpleInputService)
def mockSimpleOutputService = Mock(SimpleOutputService)

def setup(){
    def router = new SimpleRouter()
    router.simpleInputService = mockSimpleInputService
    router.simpleOutputService = mockSimpleOutputService
}

Next, we need to register this router within a minimal Camel context so that we can send messages through the routes. To do that we create a DefaultCamelContext and use some Camel testing methods to Mock the endpoints that we don’t want to actually process messages. For this use case, let’s imagine that the “log:out” endpoint is something that shouldn’t get invoked during unit testing. I recommend doing this in the setup method as Spock invokes that before each test case.

context = new DefaultCamelContext()
context.addRoutes(router)
context.start()

context.routeDefinitions.toList().each { RouteDefinition routeDefinition ->
    routeDefinition.adviceWith(context as ModelCamelContext, new AdviceWithRouteBuilder() {
        @Override
        void configure() throws Exception {
            mockEndpointsAndSkip('log:out')
        }
    })
}

## Test Cases

Ok, we’ve mocked the services and the integration endpoint, now let’s start the tests. First, register the mocked endpoint so you can assert your conditions. Second, register a producer template in the context so you can send messages through your routes. Finally, we can send a message through the route and use some Spock mock asserts to verify the message did not get sent to the test-output route.

def 'test -- empty message body with no headers'(){
    given:
        def mockTestOutputEndpoint = MockEndpoint.resolve(context, 'mock:log:out')
        mockTestOutputEndpoint.expectedCount = 0
        def producer = context.createProducerTemplate()
    when:
        producer.sendBody('direct:test-input', '')
    then:
        1 * mockSimpleInputService.performSimpleStringTask('')
        0 * mockSimpleOutputService.performSomeOtherSimpleStringTask(_)
        mockTestOutputEndpoint.assertIsSatisfied()
}

Now to prove a message with the proper header can get through to the “log:out” endpoint and invoke the proper services…

def 'test -- empty message body && valid output header'(){
    given:
        def mockTestOutputEndpoint = MockEndpoint.resolve(context, 'mock:log:out')
        mockTestOutputEndpoint.expectedCount = 1
        def producer = context.createProducerTemplate()
    when:
        producer.sendBodyAndHeaders('direct:test-input', '', ['SEND_OUT': true])
    then:
        1 * mockSimpleInputService.performSimpleStringTask('')
        1 * mockSimpleOutputService.performSomeOtherSimpleStringTask(_)
        mockTestOutputEndpoint.assertIsSatisfied()
}

And that’s it. We have used the Spock testing framework to unit test your Camel routing logic.

## Cleanup

Since we are manually making the Camel context, we need to clean up after ourselves after every test. Calling .stop on the context in the cleanup method is enough cleanup for the purposes of these unit tests.

def cleanup(){
  context.stop()
}

# Code Readability

There are a lot optimizations that can be made to the Spock specification to reduce code duplication and increase overall readability of the tests.

Using non-static strings to define endpoints to be mocked can lead to headaches if the router is refactored later. I tend to define the Endpoint URIs as static final strings in a utility class so that I can use that reference in both the router class and any subsequent test classes.

Resolving mocked endpoints in the ‘given’ block of each test is also inefficient and adds more repeated code per test. I usually define the MockEndpoint objects for the entire Specification and resolve them in the ‘setup’ Spock method. If you decide to define these global endpoints, be sure to reset them in the ‘cleanup’ method or it could lead to assertions not being resolved properly during testing.

# Reference

For a look at the project, feel free to look at the complete project on my Github or if you are just interested in the test cases, take a look at the complete Specification.

Share this Post

Related Blog Posts

JVM

Intro to Reactive Web in Spring 5

July 18th, 2017

Intro to Reactive Web in Spring 5

Mike Plummer
JVM

Implement a GraphQL Endpoint in a Groovy App

July 6th, 2017

Implement a GraphQL Endpoint in a Groovy App

Matt Schroeder
JVM

Exciting things in Spring Boot 2

June 27th, 2017

I looked at the release notes for Spring Boot 2.0.0.M1 not expecting a lot of exciting changes but was surprised by some compelling things. There is the usual “deprecated classes are removed” and “Dependency X is upgraded” but there are some more…

Mike Hostetler

About the author

Chris Tosspon

Sr. Consultant

Chris is a developer passionate about solving complex problems. With his knowledge of cryptography and advanced hardware attacks, he pursued the life of a hardware hacker before turning his attention on software development and engineering.

Chris builds applications using Spring, Java, Groovy, and has experience with most of the popular Java testing frameworks, e.g. Spock, TestNG, JUnit.