How to write unit tests for Camel Routes in Spring Boot?
Camel Spring Boot Test (camel-test-spring-junit5) is a powerful component but a bit confusing in the beginning. I think I had the similar feelings with Eric Helgeson, as he describes
A mock in camel could/should be called a
Proxy
orSpy
- not a Mock.
in his blog.
Anyway, camel-test-spring-junit5 has a great feature set and hides most of the heavy lifting. In this simple project, I used a few of these features. I hope this will be a good starting point for your implementations. One important note, I will be using mock, replace and adviced keywords interchangeably.
There are 13 unit tests for Camel Routes. I will be covering the following methods/features:
- AdviceWith.adviceWith
- AdviceWith.replaceFromWith
- AdviceWithRouteBuilder.weaveById
- AdviceWithRouteBuilder.weaveByToUri
- ProducerTemplate
- FluentProducerTemplate
- MockEndpoint.expectedMessageCount
- MockEndpoint.assertIsSatisfied
- CamelContext
- @EndpointInject
- MockEndpoint
- ConstantExpression
First of all, the environment. I used STS, Camel, camel-test-spring-junit5 and mockito. The source code is avaliable in this git repo.
What is in the project? There are 4 sample routes.

- 1_route_timerSWBatch: A simple Timer which is triggered every 10 seconds and makes a call to “direct:GetStarWarsPeople” end point.

2. 2_route_GetStarWarsPeople: This one calls StarWars people API and formats each person object in the response (in a single processor). The next step is saving each StarWarsPerson separately (using split()).

3. 3_route_FormatStarWarsPeopleRoute: This route is responsible from formatting each StarWarsPerson in the list.

4. 4_route_SaveStarWarsPersonRoute: This routes mimics the save functionality. There is no DB, so it just returns the given StarWarsPerson object.
I will try to explain some of the unit tests in detail and the way I use/understand them. Let’s start with the first one.
- StarWarsOperationsBatchTest.test_1_StarWarsOperationsBatch:
What is tested: “direct:GetStarWarsPeople” is called only once, when the timer is triggered.
When you run this unit test, in the console, you will see 2 route definitions.

The first one is the original route.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<route xmlns="http://camel.apache.org/schema/spring" customId="true" id="1_route_timerSWBatch">
<from uri="timer://swBatch?period=10000"/>
<process customId="true" id="ID_Processpor_GetStarWarsPeople"/>
<process/>
</route>
The second one is the adviced (mocked/modified) route. In fact, in the test context (Camel Context), we are running the second route.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<route xmlns="http://camel.apache.org/schema/spring" customId="true" id="1_route_timerSWBatch">
<from uri="direct:mockEndPointFor_StarWarsOperationsBatch_route_timerSWBatch"/>
<to uri="mock://mockEndPointFor_direct_GetStarWarsPeople"/>
<process/>
</route>
If you compare the XMLs line by line, it is easy to see that
- <from uri=”timer://swBatch?period=10000"/> is replaced with <from uri=”direct:mockEndPointFor_StarWarsOperationsBatch_route_timerSWBatch”/>
- <process customId=”true” id=”ID_Processpor_GetStarWarsPeople”/> is replaced with <to uri=”mock://mockEndPointFor_direct_GetStarWarsPeople”/>
These replacements are done according to the implementation in the AdviceWith.adviceWith( …..) section in this test method.
...
AdviceWith.adviceWith(camelContext, StarWarsOperationsBatch.ROUTE_ID, routeBuilder -> {
//"from("timer://swBatch?period=10000")" route is replaced with "direct:mockEndPointFor_StarWarsOperationsBatch_route_timerSWBatch".
routeBuilder.replaceFromWith("direct:mockEndPointFor_StarWarsOperationsBatch_route_timerSWBatch");
//Processor for calling "direct:GetStarWarsPeople" is mocked with mockEndPointFor_direct_GetStarWarsPeople.
routeBuilder.weaveById(StarWarsOperationsBatch.ID_Processor_GetStarWarsPeople).replace().to(mockEndPointFor_direct_GetStarWarsPeople);
});
...
2. FormatStarWarsPeopleRouteTest.test_2_FormatAllPeople
What is tested: “direct:FormatStarWarsPeople” route is behaving as expected.
In this test method “FluentProducerTemplate” is used to call “direct:FormatStarWarsPeople” route. With “FluentProducerTemplate”, we can directly get the exchange and the body. In this case, body includes the “formatted” people list. As the last step, “formatted” people list is validated against the expected results.
...
Exchange responseExchange = fluentProducerTemplate.to("direct:FormatStarWarsPeople")
.withBody(starWarsPeopleList)
.request(Exchange.class);
List<StarWarsPerson> modifiedPeople = responseExchange.getMessage().getBody(List.class);
Assertions.assertEquals(starWarsPeopleList.size(), modifiedPeople.size(), "number of items array is not correct!");
Assertions.assertEquals("fromMockPeopleApi_name0-M".toUpperCase(), modifiedPeople.get(0).getName(), "modified name of 0. item is not correct!");
Assertions.assertEquals("fromMockPeopleApi_name1-M".toUpperCase(), modifiedPeople.get(1).getName(), "modified name of 1. item is not correct!");
...
3. GetStarWarsPeopleRouteTest. test_3_Route_GetStarWarsPeople_with_constant_response_with_producerTemplate()
What is tested: There is no assertions or verify step in this test.
I am just mocking call to “https://swapi.dev/api” end point with AdviceWithRouteBuilder.weaveByToUri and returning a constant response. I think this one is quite self explanotary.
...
AdviceWith.adviceWith(
camelContext, // In this camel context,
GetStarWarsPeopleRoute.ROUTE_ID, // I want to "modify (mock)" this route with the following rules.
routeBuilder -> {
GetPeopleResponse mockGetPeopleResponse = TestHelper.getMockGetPeopleResponseWith2People();
ObjectMapper mapper = new ObjectMapper();
routeBuilder
.weaveByToUri("https://swapi.dev/api*") // this is the end point (which I want to modify)
.replace() // I want to replace (mock) this end point.
.setBody( // with this body.
new ConstantExpression(mapper.writeValueAsString(mockGetPeopleResponse))
);
});
camelContext.start();
...
4. GetStarWarsPeopleRouteTest. test_4_Route_GetStarWarsPeople_with_constant_response_2_people()
What is tested: When the body has 2 StarWarsPerson, then “direct:SaveStarWarsPerson” end point must be called twice.
Call to “https://swapi.dev/api” is mocked with a constant response (which has 2 StarWarsPerson). Call to “direct:SaveStarWarsPerson” is replaced with a mock end point. Just a note, “direct:FormatStarWarsPeople” is not mocked so this route will be called as it is.
The last step is assertions. These are implemented using MockEndpoint.expectedMessageCount, MockEndpoint.assertIsSatisfied methods.
The original route:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<route xmlns="http://camel.apache.org/schema/spring" customId="true" id="2_route_GetStarWarsPeople">
<from uri="direct:GetStarWarsPeople"/>
<process/>
<to uri="https://swapi.dev/api/people"/>
<process/>
<to customId="true" id="direct_FormatStarWarsPeople_in_GetStarWarsPeopleRoute" uri="direct:FormatStarWarsPeople"/>
<split>
<simple>${body}</simple>
<to uri="direct:SaveStarWarsPerson"/>
</split>
</route>
The modified route: You can see that “direct:FormatStarWarsPeople” is not mocked.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<route xmlns="http://camel.apache.org/schema/spring" customId="true" id="2_route_GetStarWarsPeople">
<from uri="direct:GetStarWarsPeople"/>
<process/>
<setBody>
<constant>{"count":2,"next":"mockNextPageURL","previous":"mockPreviousPageURL","results":[{...}]}</constant>
</setBody>
<process/>
<to customId="true" id="direct_FormatStarWarsPeople_in_GetStarWarsPeopleRoute" uri="direct:FormatStarWarsPeople"/>
<split>
<simple>${body}</simple>
<to uri="mock://mockEndPointFor_direct_SaveStarWarsPerson"/>
</split>
</route>
5. GetStarWarsPeopleRouteTest. test_5_Route_GetStarWarsPeople_with_no_person
What is tested: When there is no StarWarsPerson in the body, “direct:FormatStarWarsPeople” must be called once, but there must be no call to “direct:SaveStarWarsPerson” end point.
In this method, all 3 end points are mocked using AdviceWith.adviceWith, AdviceWithRouteBuilder.weaveByToUri and AdviceWithRouteBuilder. weaveById methods.
...
AdviceWith.adviceWith(camelContext, GetStarWarsPeopleRoute.ROUTE_ID, routeBuilder -> {
GetPeopleResponse mockGetPeopleResponse = TestHelper.getMockEmptyGetPeopleResponse();
ObjectMapper mapper = new ObjectMapper();
routeBuilder.weaveByToUri("https://swapi.dev/api*").replace().setBody(new ConstantExpression(mapper.writeValueAsString(mockGetPeopleResponse)));
routeBuilder.weaveById(GetStarWarsPeopleRoute.ID_FOR_direct_FormatStarWarsPeople).replace().to(mockEndPointFor_direct_FormatStarWarsPeople);
routeBuilder.weaveByToUri("direct:SaveStarWarsPerson").replace().to(mockEndPointFor_direct_SaveStarWarsPerson);
});
mockEndPointFor_direct_FormatStarWarsPeople.expectedMessageCount(1);
mockEndPointFor_direct_SaveStarWarsPerson.expectedMessageCount(0);
camelContext.start();
...
For the last time, let’s have a look at the route XMLs.
Original one:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<route xmlns="http://camel.apache.org/schema/spring" customId="true" id="2_route_GetStarWarsPeople">
<from uri="direct:GetStarWarsPeople"/>
<process/>
<to uri="https://swapi.dev/api/people"/>
<process/>
<to customId="true" id="direct_FormatStarWarsPeople_in_GetStarWarsPeopleRoute" uri="direct:FormatStarWarsPeople"/>
<split>
<simple>${body}</simple>
<to uri="direct:SaveStarWarsPerson"/>
</split>
</route>
Modified route: All the end points are mocked.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<route xmlns="http://camel.apache.org/schema/spring" customId="true" id="2_route_GetStarWarsPeople">
<from uri="direct:GetStarWarsPeople"/>
<process/>
<setBody>
<constant>{"count":0,"next":"mockNextPageURL","previous":"mockPreviousPageURL","results":[]}</constant>
</setBody>
<process/>
<to uri="mock://mockEndPointFor_direct_FormatStarWarsPeople"/>
<split>
<simple>${body}</simple>
<to uri="mock://mockEndPointFor_direct_SaveStarWarsPerson"/>
</split>
</route>
6. GetStarWarsPeopleRouteTestWithServiceMocks. test_9_Route_GetStarWarsPeople_with_mockEndPointFor_To_getPeopleAPI
What is tested: When there is 2 people in the list, “direct:FormatStarWarsPeople” end point must be called once and StarWarsPeopleService.saveStarWarsPeople service method must be called twice. As the last verify step, Assertions.assertEquals, Mockito.verify and mockito.ArgumentCaptor methods are used.
Note: There is nothing new in this test method. You can use Camel test features with the other unit test frameworks.
In this method, “https://swapi.dev/api” end point is mocked with constant 2 StarWarsPerson and “direct:FormatStarWarsPeople” is replaced with mock end point.
The “direct:SaveStarWarsPerson” end point itself is NOT mocked. However, StarWarsPeopleService.saveStarWarsPeople service component is mocked.
Full test method:
/*
Camel related items:
AdviceWith.adviceWith
AdviceWithRouteBuilder.weaveByToUri
AdviceWithRouteBuilder.weaveById
FluentProducerTemplate
MockEndpoint.expectedMessageCount
MockEndpoint.assertIsSatisfied
Description:
Api call to "https://swapi.dev/api" is mocked with constant response (with 2 people in the list).
Call to direct:FormatStarWarsPeople mocked with mock end point using weaveById (so no formatting is applied on the list items).
StarWarsPeopleService.saveStarWarsPeople is mocked.
StarWarsPeopleService.saveStarWarsPeople is verified to be called 2 times.
*/
@Test
public void test_9_Route_GetStarWarsPeople_with_mockEndPointFor_To_getPeopleAPI() throws Exception {
StarWarsPerson dummyStarWarsPeople = new StarWarsPerson();
dummyStarWarsPeople.setName("dummyStarWarsPeople_name");
Mockito.when(mockPeopleService.saveStarWarsPeople(ArgumentMatchers.any(StarWarsPerson.class))).thenReturn(dummyStarWarsPeople);
AdviceWith.adviceWith(camelContext, GetStarWarsPeopleRoute.ROUTE_ID, routeBuilder -> {
/*
The name for the first item is fromMockPeopleApi_name0.
The name for the second items is fromMockPeopleApi_name1.
*/
GetPeopleResponse mockGetPeopleResponse = TestHelper.getMockGetPeopleResponseWith2People();
ObjectMapper mapper = new ObjectMapper();
routeBuilder.weaveByToUri("https://swapi.dev/api*").replace().setBody(new ConstantExpression(mapper.writeValueAsString(mockGetPeopleResponse)));
// no formatting is applied.
routeBuilder.weaveById(GetStarWarsPeopleRoute.ID_FOR_direct_FormatStarWarsPeople).replace().to(mockEndPointFor_direct_FormatStarWarsPeople);
});
mockEndPointFor_direct_FormatStarWarsPeople.expectedMessageCount(1);
camelContext.start();
Exchange resultFromRebuiltGetStarWarsPeople = fluentProducerTemplate.to("direct:GetStarWarsPeople").request(Exchange.class);
mockEndPointFor_direct_FormatStarWarsPeople.assertIsSatisfied();
Mockito.verify(mockPeopleService, Mockito.times(2)).saveStarWarsPeople(ArgumentMatchers.any());
Mockito.verify(mockPeopleService, Mockito.times(2)).saveStarWarsPeople(starWarsPeopleCaptor.capture());
List<StarWarsPerson> capturedStarWarsPeopleList = starWarsPeopleCaptor.getAllValues();
// There must be 2 captured parameters for StarWarsPeopleService.saveStarWarsPeople.
Assertions.assertEquals(2, capturedStarWarsPeopleList.size());
Assertions.assertEquals("fromMockPeopleApi_name0", capturedStarWarsPeopleList.get(0).getName(), "modified name of 0. item is not correct!");
Assertions.assertEquals("fromMockPeopleApi_name1", capturedStarWarsPeopleList.get(1).getName(), "modified name of 1. item is not correct!");
}
I will not continue with the other test methods. You can get the full implementation from git repo.
This was a quite long article. Thanks for reading.