Contents
- Why Test Camel Routes
- Dependencies
- CamelTestSupport
- MockEndpoint
- ProducerTemplate
- AdviceWith
- Testing with Spring Boot
- NotifyBuilder
- Testing Error Handling
- Best Practices
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, wires your routes, and gives you mock endpoints, producer templates, and advice tools to verify every aspect of a route without connecting to real databases, message brokers, or HTTP services.
Camel tests start a real CamelContext and execute the route logic in-process. They are closer to integration tests than unit tests — the route DSL, type converters, and EIP processors all run for real.
Add the Camel test module that matches your test framework. For JUnit 5 (recommended), use camel-test-junit5. If you are running inside a Spring Boot application, use camel-test-spring-junit5 instead — it integrates with the Spring test context.
<!-- 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 separately — it is included transitively.
Make sure the Camel test module version matches your camel-core version exactly. Version mismatches between the runtime and test modules cause class-not-found errors at startup.
CamelTestSupport is the base class for plain Camel tests (no Spring). It creates a fresh CamelContext for each test method, starts it, and shuts it down afterwards. Override createRouteBuilder() to define the route under test.
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 template field is a ProducerTemplate that CamelTestSupport creates automatically. Use it to send messages into direct: or seda: endpoints that trigger your route. The getMockEndpoint() method returns a MockEndpoint reference for any mock: URI in the route.
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
}
CamelTestSupport creates a new CamelContext per test method by default. If your tests are slow because of context startup, override isUseRouteBuilder() to return false and manually control route creation — but be careful with shared state between tests.
MockEndpoint is the most important testing tool in Camel. It records every exchange that arrives, and you can set expectations before sending test messages — then call assertIsSatisfied() to verify that all expectations were met.
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() waits up to 10 seconds by default for asynchronous routes. You can change the timeout with mock.setResultWaitTime(5000) or pass a timeout directly: mock.assertIsSatisfied(3000).
ProducerTemplate sends test messages into routes. CamelTestSupport provides one via the template field. In Spring Boot tests, inject it with @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();
}
Use sendBody() for InOnly (one-way) routes and requestBody() for InOut (request-reply) routes. Using the wrong pattern causes either missing responses or unexpected exchange patterns in the route.
AdviceWith lets you modify an existing route before it starts — replace real endpoints with mocks, add interceptors, remove processors, or inject test logic at any point. This is essential when testing production routes that connect to external systems.
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() with a mock |
| 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() endpoint (useful for replacing timer or JMS consumers) |
@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();
}
When using AdviceWith, you must override isUseAdviceWith() to return true and call context.start() manually in each test after the advice is applied. Forgetting this causes the route to start unmodified before your advice runs.
When your Camel routes run inside Spring Boot, use @CamelSpringBootTest instead of extending CamelTestSupport. This boots the full Spring context and auto-discovers your route beans.
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();
}
}
With @MockEndpoints("kafka:*"), Camel creates parallel mock endpoints named mock:kafka:topicName. Messages are still sent to the real endpoint unless you use @MockEndpointsAndSkip. Use the skip variant to avoid needing a running Kafka broker during tests.
NotifyBuilder lets you wait for specific conditions to be met across one or more routes before asserting results. It is useful for asynchronous routes where you cannot predict exactly when processing completes.
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");
}
NotifyBuilder is essential for testing seda:, timer:, and other asynchronous routes. Without it you would need arbitrary Thread.sleep() calls that make tests slow and flaky.
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);
}
}
Always set redeliveryDelay(0) in test routes to avoid slowing down the test suite. Real redelivery delays of seconds or minutes make test execution unbearably slow.
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 direct: endpoints as entry points so tests can send messages synchronously
- Reset any shared counters or state in a @BeforeEach method
- Avoid static fields that accumulate state across test methods
Mock External Systems
- Use AdviceWith and mockEndpointsAndSkip() to replace HTTP, Kafka, JMS, and database endpoints
- Never require a running external service for route-level tests — save that for end-to-end tests
- Use replaceFromWith("direct:test") to replace timer or polling consumers with on-demand triggers
- 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 src/test/resources using the Camel resource helper
- 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: testInvalidOrderIsRoutedToDlq() not test1()
- Assign routeId() to every production route so you can reference them in AdviceWith and NotifyBuilder
- 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 redeliveryDelay(0) in test error handlers to eliminate unnecessary wait times
- Use @DisableJmx to skip JMX registration and speed up context startup
- Avoid creating new CamelContext instances when a single context with multiple routes will do
- Set short timeouts on assertIsSatisfied() — if a test needs more than a few seconds, something is wrong
// 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);
}
}
A good rule of thumb: if your test requires more than 5 seconds to run, either your route is doing too much for a single test, or you are missing a mock somewhere and hitting a real (slow) endpoint.