Contents

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:

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
MethodDescription
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
MethodDescription
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
OperationDescription
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
AnnotationDescription
@CamelSpringBootTestEnables 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
@UseAdviceWithPrevents auto-start of routes — required when using AdviceWith in Spring tests
@DisableJmxDisables 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
MethodDescription
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
Mock External Systems
Test Data
Naming and Organization
Performance
// 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.