Apache Camel: Testing Routes
Camel ships with a powerful test kit that lets you verify route logic, mock external endpoints, intercept and reroute messages, and assert message content: all without starting real infrastructure. This guide covers CamelTestSupport, MockEndpoint, AdviceWith, NotifyBuilder, and Spring Boot test annotations.
Contents
Why Test Camel Routes
Camel routes are integration code: they connect systems, transform data, and orchestrate workflows. Unlike pure business logic that can be tested with simple unit tests, route logic depends on the Camel runtime, the EIP implementations, and the interaction between processors. Without proper route tests you cannot be confident that:
- Messages are routed to the correct destination based on content or headers
- Transformations produce the expected output format
- Error handling, retries, and dead letter channels behave as configured
- Filters, splitters, and aggregators process messages correctly
- Route startup ordering and dependency wiring work end to end
Camel provides a dedicated test framework that boots a lightweight CamelContext
CamelContextDependencies
Add the Camel test module that matches your test framework. For JUnit 5 (recommended), use camel-test-junit5camel-test-spring-junit5
<!-- Plain Camel (no Spring) — JUnit 5 -->
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-test-junit5</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot + Camel — JUnit 5 -->
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-test-spring-junit5</artifactId>
<scope>test</scope>
</dependency>
Both modules pull in the core mock and test utilities automatically. You do not need to add camel-mock
camel-coreCamelTestSupport
CamelTestSupportCamelContextcreateRouteBuilder()
import org.apache.camel.RoutesBuilder;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Test;
public class SimpleRouteTest extends CamelTestSupport {
@Override
protected RoutesBuilder createRouteBuilder() {
return new RouteBuilder() {
@Override
public void configure() {
from("direct:start")
.transform(body().prepend("Hello, "))
.to("mock:result");
}
};
}
@Test
void testTransform() throws Exception {
MockEndpoint mock = getMockEndpoint("mock:result");
mock.expectedMessageCount(1);
mock.expectedBodiesReceived("Hello, World");
template.sendBody("direct:start", "World");
mock.assertIsSatisfied();
}
}
The templateProducerTemplateCamelTestSupportdirect:seda:getMockEndpoint()MockEndpointmock:
If your route under test is defined in a separate class, return an instance of that class from createRouteBuilder()
@Override
protected RoutesBuilder createRouteBuilder() {
return new OrderProcessingRoute(); // your production RouteBuilder
}
CamelTestSupportCamelContextisUseRouteBuilder()falseMockEndpoint
MockEndpointassertIsSatisfied()
Common Expectations
| Method | Description |
|---|---|
expectedMessageCount(int) | Exactly N messages must arrive |
expectedMinimumMessageCount(int) | At least N messages must arrive |
expectedBodiesReceived(Object...) | Bodies must match in order |
expectedBodiesReceivedInAnyOrder(Object...) | Bodies must match regardless of order |
expectedHeaderReceived(String, Object) | A specific header key-value must be present |
message(int).body().isEqualTo(Object) | Assert on a specific message by index |
expectedPropertyReceived(String, Object) | A specific exchange property must be present |
@Test
void testFilterAndTransform() throws Exception {
MockEndpoint validOrders = getMockEndpoint("mock:validOrders");
MockEndpoint invalidOrders = getMockEndpoint("mock:invalidOrders");
validOrders.expectedMessageCount(2);
invalidOrders.expectedMessageCount(1);
// valid orders have amount > 0
validOrders.expectedBodiesReceivedInAnyOrder(
"{\"id\":1,\"amount\":100}",
"{\"id\":3,\"amount\":250}"
);
template.sendBody("direct:orders", "{\"id\":1,\"amount\":100}");
template.sendBody("direct:orders", "{\"id\":2,\"amount\":-5}");
template.sendBody("direct:orders", "{\"id\":3,\"amount\":250}");
validOrders.assertIsSatisfied();
invalidOrders.assertIsSatisfied();
}
You can also use predicates for flexible assertions:
@Test
void testWithPredicates() throws Exception {
MockEndpoint mock = getMockEndpoint("mock:result");
mock.expectedMessageCount(1);
// Assert that the body contains a substring
mock.message(0).body(String.class).contains("processed");
// Assert a header exists and has a specific pattern
mock.message(0).header("correlationId").isNotNull();
mock.message(0).header("status").isEqualTo("COMPLETE");
template.sendBodyAndHeader("direct:start", "to-be-processed", "orderId", "ORD-123");
mock.assertIsSatisfied();
}
assertIsSatisfied()mock.setResultWaitTime(5000)mock.assertIsSatisfied(3000)ProducerTemplate
ProducerTemplateCamelTestSupporttemplate@Autowired
Sending Methods
| Method | Description |
|---|---|
sendBody(endpoint, body) | Fire-and-forget: sends a message with InOnly pattern |
sendBodyAndHeader(endpoint, body, key, value) | Send with a single header |
sendBodyAndHeaders(endpoint, body, headers) | Send with multiple headers (Map) |
requestBody(endpoint, body) | Request-reply: sends InOut and returns the response body |
requestBodyAndHeader(endpoint, body, key, value) | Request-reply with a header |
@Test
void testRequestReply() throws Exception {
// requestBody sends InOut — the route must set a response body
String result = template.requestBody("direct:greet", "Alice", String.class);
assertEquals("Hello, Alice!", result);
}
@Test
void testSendWithHeaders() throws Exception {
MockEndpoint mock = getMockEndpoint("mock:result");
mock.expectedMessageCount(1);
mock.expectedHeaderReceived("priority", "HIGH");
Map<String, Object> headers = new HashMap<>();
headers.put("priority", "HIGH");
headers.put("source", "test");
template.sendBodyAndHeaders("direct:start", "urgent message", headers);
mock.assertIsSatisfied();
}
sendBody()requestBody()AdviceWith
AdviceWith
import org.apache.camel.builder.AdviceWith;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Test;
public class AdviceWithTest extends CamelTestSupport {
@Override
public boolean isUseAdviceWith() {
return true; // REQUIRED — prevents routes from starting before advice is applied
}
@Override
protected RoutesBuilder createRouteBuilder() {
return new RouteBuilder() {
@Override
public void configure() {
from("direct:start").routeId("myRoute")
.to("http://external-api/orders")
.transform(body().append(" enriched"))
.to("kafka:orders-topic");
}
};
}
@Test
void testWithMockedEndpoints() throws Exception {
// Replace the HTTP and Kafka endpoints with mocks
AdviceWith.adviceWith(context, "myRoute", route -> {
route.interceptSendToEndpoint("http://external-api/orders")
.skipSendToOriginalEndpoint()
.setBody(constant("order-data"));
route.weaveByToUri("kafka:*")
.replace()
.to("mock:kafka");
});
// Start the context AFTER applying advice
context.start();
MockEndpoint mockKafka = getMockEndpoint("mock:kafka");
mockKafka.expectedMessageCount(1);
mockKafka.expectedBodiesReceived("order-data enriched");
template.sendBody("direct:start", "test");
mockKafka.assertIsSatisfied();
}
}
Common AdviceWith Operations
| Operation | Description |
|---|---|
mockEndpoints() | Auto-mock all endpoints: each endpoint gets a parallel mock:<scheme>:... |
mockEndpointsAndSkip("http*") | Mock matching endpoints and skip sending to the real ones |
weaveByToUri("kafka:*").replace().to("mock:kafka") | Replace a specific to() |
weaveById("processorId").before().to("mock:before") | Insert a mock before a named processor |
weaveById("processorId").remove() | Remove a processor from the route entirely |
weaveAddFirst().to("mock:input") | Add a step at the very beginning of the route |
weaveAddLast().to("mock:output") | Add a step at the very end of the route |
replaceFromWith("direct:test") | Replace the from() |
@Test
void testReplaceFromEndpoint() throws Exception {
// Replace a timer-triggered route with a direct endpoint for testing
AdviceWith.adviceWith(context, "scheduledRoute", route -> {
route.replaceFromWith("direct:trigger");
route.mockEndpointsAndSkip("jdbc:*");
});
context.start();
MockEndpoint mockDb = getMockEndpoint("mock:jdbc:dataSource");
mockDb.expectedMessageCount(1);
template.sendBody("direct:trigger", "test-data");
mockDb.assertIsSatisfied();
}
AdviceWithisUseAdviceWith()truecontext.start()Testing with Spring Boot
When your Camel routes run inside Spring Boot, use @CamelSpringBootTestCamelTestSupport
import org.apache.camel.CamelContext;
import org.apache.camel.ProducerTemplate;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.test.spring.junit5.CamelSpringBootTest;
import org.apache.camel.test.spring.junit5.MockEndpoints;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
@CamelSpringBootTest
@SpringBootTest
@MockEndpoints("kafka:*") // auto-mock all Kafka endpoints
public class OrderRouteSpringTest {
@Autowired
private CamelContext camelContext;
@Autowired
private ProducerTemplate template;
@Test
void testOrderRouting() throws Exception {
MockEndpoint mockKafka = camelContext.getEndpoint(
"mock:kafka:orders-processed", MockEndpoint.class
);
mockKafka.expectedMessageCount(1);
mockKafka.expectedHeaderReceived("orderStatus", "VALIDATED");
template.sendBody("direct:newOrder", "{\"id\":1,\"item\":\"Widget\",\"qty\":5}");
mockKafka.assertIsSatisfied();
}
}
Key Spring Boot Test Annotations
| Annotation | Description |
|---|---|
@CamelSpringBootTest | Enables Camel test support in a Spring Boot test class |
@MockEndpoints("pattern") | Auto-mock endpoints matching the pattern: messages still flow to originals |
@MockEndpointsAndSkip("pattern") | Auto-mock and skip sending to the real endpoints |
@UseAdviceWith | Prevents auto-start of routes: required when using AdviceWith in Spring tests |
@DisableJmx | Disables JMX to speed up test startup |
import org.apache.camel.builder.AdviceWith;
import org.apache.camel.test.spring.junit5.CamelSpringBootTest;
import org.apache.camel.test.spring.junit5.UseAdviceWith;
import org.springframework.boot.test.context.SpringBootTest;
@CamelSpringBootTest
@SpringBootTest
@UseAdviceWith
public class AdviceWithSpringTest {
@Autowired
private CamelContext camelContext;
@Autowired
private ProducerTemplate template;
@Test
void testWithAdvice() throws Exception {
AdviceWith.adviceWith(camelContext, "paymentRoute", route -> {
route.mockEndpointsAndSkip("http://payment-gateway/*");
route.weaveAddLast().to("mock:final");
});
camelContext.start();
MockEndpoint mockFinal = camelContext.getEndpoint("mock:final", MockEndpoint.class);
mockFinal.expectedMessageCount(1);
template.sendBody("direct:pay", "{\"amount\":99.95}");
mockFinal.assertIsSatisfied();
}
}
@MockEndpoints("kafka:*")mock:kafka:topicName@MockEndpointsAndSkipNotifyBuilder
NotifyBuilder
import org.apache.camel.builder.NotifyBuilder;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Test;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class NotifyBuilderTest extends CamelTestSupport {
@Override
protected RoutesBuilder createRouteBuilder() {
return new RouteBuilder() {
@Override
public void configure() {
from("seda:asyncInput")
.delay(500)
.transform(body().prepend("async-"))
.to("mock:asyncResult");
}
};
}
@Test
void testWaitForCompletion() throws Exception {
// Wait until 3 messages have completed on ANY route
NotifyBuilder notify = new NotifyBuilder(context)
.whenDone(3)
.create();
template.sendBody("seda:asyncInput", "msg1");
template.sendBody("seda:asyncInput", "msg2");
template.sendBody("seda:asyncInput", "msg3");
// Block until condition is met or timeout
boolean done = notify.matches(10, TimeUnit.SECONDS);
assertTrue(done, "Expected 3 messages to complete within 10 seconds");
MockEndpoint mock = getMockEndpoint("mock:asyncResult");
assertEquals(3, mock.getReceivedCounter());
}
}
Common NotifyBuilder Conditions
| Method | Description |
|---|---|
whenDone(int) | Wait until N exchanges have completed (success or failure) |
whenCompleted(int) | Wait until N exchanges have completed successfully |
whenFailed(int) | Wait until N exchanges have failed |
whenBodiesDone(Object...) | Wait for exchanges with specific bodies to complete |
fromRoute("routeId") | Scope the condition to a specific route |
wereSentTo("mock:result") | Condition based on messages reaching a specific endpoint |
@Test
void testRouteSpecificNotification() throws Exception {
NotifyBuilder notify = new NotifyBuilder(context)
.fromRoute("orderRoute")
.whenCompleted(1)
.and()
.fromRoute("paymentRoute")
.whenCompleted(1)
.create();
template.sendBody("direct:placeOrder", "{\"id\":42}");
boolean allDone = notify.matches(15, TimeUnit.SECONDS);
assertTrue(allDone, "Both order and payment routes should complete");
}
NotifyBuilderseda:timer:Thread.sleep()Testing Error Handling
Testing error handling is just as important as testing the happy path. Camel makes it straightforward to verify that exceptions are caught, retries are attempted, and dead letter channels receive failed messages.
Testing Dead Letter Channel
public class DlcTest extends CamelTestSupport {
@Override
public boolean isUseAdviceWith() {
return true;
}
@Override
protected RoutesBuilder createRouteBuilder() {
return new RouteBuilder() {
@Override
public void configure() {
errorHandler(
deadLetterChannel("direct:dlq")
.maximumRedeliveries(2)
.redeliveryDelay(0) // no delay in tests
);
from("direct:start").routeId("dlcRoute")
.process(exchange -> {
throw new RuntimeException("Simulated failure");
});
from("direct:dlq").routeId("dlqRoute")
.to("mock:deadLetterQueue");
}
};
}
@Test
void testMessageSentToDlq() throws Exception {
AdviceWith.adviceWith(context, "dlcRoute", route -> { });
context.start();
MockEndpoint dlq = getMockEndpoint("mock:deadLetterQueue");
dlq.expectedMessageCount(1);
dlq.message(0).header("CamelRedelivered").isEqualTo(true);
dlq.message(0).header("CamelRedeliveryCounter").isEqualTo(2);
template.sendBody("direct:start", "will-fail");
dlq.assertIsSatisfied();
}
}
Testing onException Handling
public class OnExceptionTest extends CamelTestSupport {
@Override
protected RoutesBuilder createRouteBuilder() {
return new RouteBuilder() {
@Override
public void configure() {
onException(IllegalArgumentException.class)
.handled(true)
.setBody(constant("INVALID"))
.to("mock:errorHandler");
onException(java.io.IOException.class)
.maximumRedeliveries(3)
.redeliveryDelay(0)
.handled(true)
.setBody(constant("IO_ERROR"))
.to("mock:ioErrorHandler");
from("direct:validate")
.process(exchange -> {
String body = exchange.getMessage().getBody(String.class);
if ("bad".equals(body)) {
throw new IllegalArgumentException("Invalid input");
}
exchange.getMessage().setBody("OK: " + body);
})
.to("mock:result");
}
};
}
@Test
void testInvalidInputHandled() throws Exception {
MockEndpoint errorMock = getMockEndpoint("mock:errorHandler");
MockEndpoint resultMock = getMockEndpoint("mock:result");
errorMock.expectedMessageCount(1);
errorMock.expectedBodiesReceived("INVALID");
resultMock.expectedMessageCount(0);
template.sendBody("direct:validate", "bad");
errorMock.assertIsSatisfied();
resultMock.assertIsSatisfied();
}
@Test
void testValidInputPassesThrough() throws Exception {
MockEndpoint resultMock = getMockEndpoint("mock:result");
resultMock.expectedMessageCount(1);
resultMock.expectedBodiesReceived("OK: good");
template.sendBody("direct:validate", "good");
resultMock.assertIsSatisfied();
}
}
Testing Retry Behavior
public class RetryTest extends CamelTestSupport {
private int callCount = 0;
@Override
protected RoutesBuilder createRouteBuilder() {
return new RouteBuilder() {
@Override
public void configure() {
onException(RuntimeException.class)
.maximumRedeliveries(3)
.redeliveryDelay(0)
.handled(true)
.to("mock:exhausted");
from("direct:flaky")
.process(exchange -> {
callCount++;
if (callCount < 3) {
throw new RuntimeException("Attempt " + callCount + " failed");
}
exchange.getMessage().setBody("Success on attempt " + callCount);
})
.to("mock:result");
}
};
}
@Test
void testSucceedsAfterRetries() throws Exception {
callCount = 0;
MockEndpoint result = getMockEndpoint("mock:result");
result.expectedMessageCount(1);
result.expectedBodiesReceived("Success on attempt 3");
template.sendBody("direct:flaky", "test");
result.assertIsSatisfied();
assertEquals(3, callCount);
}
}
redeliveryDelay(0)Best Practices
Follow these guidelines to keep your Camel test suite fast, reliable, and maintainable.
Test Isolation
- Each test should create its own mock expectations: never share mock state between tests
- Use
endpoints as entry points so tests can send messages synchronouslydirect: - Reset any shared counters or state in a
method@BeforeEach - Avoid static fields that accumulate state across test methods
Mock External Systems
- Use
andAdviceWith to replace HTTP, Kafka, JMS, and database endpointsmockEndpointsAndSkip() - Never require a running external service for route-level tests: save that for end-to-end tests
- Use
to replace timer or polling consumers with on-demand triggersreplaceFromWith("direct:test") - If you must use an external system, use Testcontainers to spin up a disposable instance
Test Data
- Keep test payloads small and readable: inline them in the test when possible
- For complex XML or JSON, load from files in
using the Camel resource helpersrc/test/resources - Test edge cases: null bodies, empty strings, malformed input, very large messages
- Use meaningful test data that makes the assertion readable: avoid random UUIDs when a descriptive value works
Naming and Organization
- Name test methods to describe the scenario:
nottestInvalidOrderIsRoutedToDlq()test1() - Assign
to every production route so you can reference them inrouteId() andAdviceWithNotifyBuilder - Group tests by route: one test class per RouteBuilder or per logical route group
- Put Camel integration tests in a separate source set or Maven profile so they do not slow down unit test runs
Performance
- Set
in test error handlers to eliminate unnecessary wait timesredeliveryDelay(0) - Use
to skip JMX registration and speed up context startup@DisableJmx - Avoid creating new
instances when a single context with multiple routes will doCamelContext - Set short timeouts on
, if a test needs more than a few seconds, something is wrongassertIsSatisfied()
// Example: well-structured test following best practices
public class OrderValidationRouteTest extends CamelTestSupport {
@Override
public boolean isUseAdviceWith() {
return true;
}
@Override
protected RoutesBuilder createRouteBuilder() {
return new OrderValidationRoute(); // test the actual production route
}
@Test
void testValidOrderIsProcessed() throws Exception {
AdviceWith.adviceWith(context, "orderValidation", route -> {
route.mockEndpointsAndSkip("jdbc:*", "kafka:*");
});
context.start();
MockEndpoint kafka = getMockEndpoint("mock:kafka:orders-validated");
kafka.expectedMessageCount(1);
kafka.expectedHeaderReceived("valid", true);
template.sendBody("direct:validateOrder",
"{\"orderId\":\"ORD-001\",\"amount\":49.99,\"customer\":\"Alice\"}");
kafka.assertIsSatisfied(3000);
}
@Test
void testMissingAmountIsRejected() throws Exception {
AdviceWith.adviceWith(context, "orderValidation", route -> {
route.mockEndpointsAndSkip("jdbc:*", "kafka:*");
});
context.start();
MockEndpoint errors = getMockEndpoint("mock:kafka:orders-invalid");
errors.expectedMessageCount(1);
template.sendBody("direct:validateOrder",
"{\"orderId\":\"ORD-002\",\"customer\":\"Bob\"}");
errors.assertIsSatisfied(3000);
}
}