You unit test Apache Camel by bootstrapping a CamelContext in JUnit 5, sending messages into real endpoints (direct:, seda:, REST), and asserting behaviour via responses or MockEndpoints, while keeping production RouteBuilders free of mocks and using Camel 4–friendly testing patterns.
Core building blocks
Modern Camel testing with JUnit 5 rests on three pillars: a managed CamelContext, controlled inputs, and observable outputs.
CamelTestSupportmanages the lifecycle of the CamelContext and exposescontext,template(aProducerTemplate), andgetMockEndpoint.- You inject messages with
template.sendBody(...)ortemplate.requestBody(...)intodirect:,seda:, or HTTP endpoints - You assert via:
MockEndpointexpectations (count, body, headers, order), or- Assertions on returned bodies.
Rationale: you want tests that execute the same routing logic as production, but in a fast, in‑JVM, repeatable way.
1. Testing direct component
A good practice is: no mock: in production routes; mocks are introduced only from tests. We start with a simple transformation route.
Route: only real direct: endpoints
package com.example;
import org.apache.camel.builder.RouteBuilder;
public class UppercaseRoute extends RouteBuilder {
@Override
public void configure() {
from("direct:start") // real entry endpoint
.routeId("uppercase-route")
.transform(simple("${body.toUpperCase()}"))
.to("direct:result"); // real internal endpoint
}
}
Rationale:
direct:startis a synchronous, in‑JVM endpoint ideal as a “unit test entry point” and also usable in production wiring.direct:resultis a real internal endpoint you can “tap” from tests usingAdviceWith, keeping test concerns out of the RouteBuilder.
Test: apply AdviceWith in setup, then start context
In Camel 4, instead of overriding any flag, you apply AdviceWith in setup and then start the context explicitly.
package com.example;
import org.apache.camel.RoutesBuilder;
import org.apache.camel.builder.AdviceWith;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class UppercaseRouteTest extends CamelTestSupport {
@Override
protected RoutesBuilder createRouteBuilder() {
return new UppercaseRoute();
}
@BeforeEach
void adviseRoute() throws Exception {
// Apply advice *before* the context is fully started
AdviceWith.adviceWith(context, "uppercase-route", route -> {
route.weaveByToUri("direct:result")
.replace()
.to("mock:result");
});
// Ensure context is started after advice is applied
if (!context.isStarted()) {
context.start();
}
}
@Test
void shouldUppercaseBody() throws Exception {
// 1. Expectations on the mock consumer
MockEndpoint result = getMockEndpoint("mock:result");
result.expectedMessageCount(1);
result.expectedBodiesReceived("HELLO");
// 2. Exercise the route via a real producer
template.sendBody("direct:start", "hello");
// 3. Verify expectations
result.assertIsSatisfied();
}
}
Rationale:
- The RouteBuilder is production-pure (
direct:only); tests decide where to splice inmock:viaAdviceWith. - You apply advice in
@BeforeEachwhile the context is created but before you use it, then explicitly start it, which aligns with modern Camel 4 test support guidance.
2. Testing seda component
For asynchronous flows, seda: is a common choice. You keep the route realistic and only intercept the tail for assertions.
Route: seda: producer and consumer
package com.example;
import org.apache.camel.builder.RouteBuilder;
public class UppercaseRouteSeda extends RouteBuilder {
@Override
public void configure() {
from("seda:input") // real async entry point
.routeId("uppercase-route-seda")
.transform(simple("${body.toUpperCase()}"))
.to("seda:output"); // real async consumer endpoint
}
}
Rationale:
seda:simulates queue-like, asynchronous behaviour in‑JVM and is commonly used in real Camel topologies.
Test: intercept only the consumer side
package com.example;
import org.apache.camel.RoutesBuilder;
import org.apache.camel.builder.AdviceWith;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class UppercaseRouteSedaTest extends CamelTestSupport {
@Override
protected RoutesBuilder createRouteBuilder() {
return new UppercaseRouteSeda();
}
@BeforeEach
void adviseRoute() throws Exception {
AdviceWith.adviceWith(context, "uppercase-route-seda", route -> {
route.weaveByToUri("seda:output")
.replace()
.to("mock:result");
});
if (!context.isStarted()) {
context.start();
}
}
@Test
void shouldUppercaseBodyUsingSedaProducer() throws Exception {
MockEndpoint result = getMockEndpoint("mock:result");
result.expectedMessageCount(1);
result.expectedBodiesReceived("HELLO");
template.sendBody("seda:input", "hello");
result.assertIsSatisfied();
}
}
Rationale:
- The route used in production (
seda:input→seda:output) is unchanged. - The test uses
AdviceWithto “cut off” the external consumer and replace it withmock:result, which is precisely where you want isolation.
3. REST DSL route with internal direct: logic
REST DSL adds a mapping layer (paths, verbs, binding) over internal routes that contain the business logic. Testing is easier when those are separated.
Route: REST DSL + internal direct: route
package com.example;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.RouteDefinition;
import org.apache.camel.model.rest.RestBindingMode;
public class RestRoute extends RouteBuilder {
@Override
public void configure() {
// REST server configuration for tests / dev
restConfiguration()
.component("netty-http")
.host("localhost")
.port(8081)
.bindingMode(RestBindingMode.off);
// Internal route with business logic
configureHelloRoute();
// REST DSL: GET /api/hello/{name} - routes to the separate direct route
rest("/api")
.get("/hello/{name}")
.routeId("rest-hello-route")
.produces("application/json")
.to("direct:hello");
}
/**
* Configures the hello route with business logic.
* This method is extracted to allow testing the route logic independently.
*/
protected RouteDefinition configureHelloRoute() {
return from("direct:hello")
.routeId("direct-hello-route")
.log("Processing direct:hello with headers: ${headers}")
.setBody(simple("{\"message\": \"Hello, ${header.name}!\"}"))
.setHeader("Content-Type", constant("application/json"));
}
}
Rationale:
configureHelloRoute()encapsulates the business logic in a reusable method that always createsfrom("direct:hello"). This gives you a stable seam for unit tests: any test that callsconfigureHelloRoute()will have a validdirect:helloconsumer.- The main
configure()wires REST to that internal route, which is the transport layer. By keeping this wiring inconfigure()and the logic inconfigureHelloRoute(), you can selectively enable or bypass the REST layer in tests without duplicating code.
Note: using RestBindingMode.off is a pragmatic choice here, because the GET action does not carry a request body and you are constructing the JSON response yourself. This avoids any extra marshalling/unmarshalling machinery and keeps the example simple and predictable.
4. Unit test for REST internal route (direct:hello)
This test bypasses HTTP and focuses on the business logic behind the REST endpoint.
package com.example;
import org.apache.camel.RoutesBuilder;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* This test validates the business logic of RestRoute by testing the direct:hello route
* using the actual RestRoute class, bypassing REST server configuration.
*/
class RestRouteDirectTest extends CamelTestSupport {
@Override
protected RoutesBuilder createRouteBuilder() {
// Create a test-specific version of RestRoute that only configures the business logic
return new RestRoute() {
@Override
public void configure() {
// Only configure the hello route, skip REST server configuration
configureHelloRoute();
}
};
}
@Test
void shouldReturnGreetingForName() {
String response = template.requestBodyAndHeader(
"direct:hello",
null,
"name",
"Alice",
String.class
);
assertEquals("{\"message\": \"Hello, Alice!\"}", response);
}
@Test
void shouldReturnGreetingForDifferentName() {
String response = template.requestBodyAndHeader(
"direct:hello",
null,
"name",
"Bob",
String.class
);
assertEquals("{\"message\": \"Hello, Bob!\"}", response);
}
}
Rationale:
- Testing
direct:hellodirectly gives you a fast, deterministic unit test with no HTTP stack involved. - You reuse the exact same logic (
configureHelloRoute()) that production uses, so there is no “test-only” copy of the route. - By overriding
configure()and calling onlyconfigureHelloRoute(), you intentionally skiprestConfiguration()andrest("/api")..., which keeps this test focused solely on the business logic and avoids starting an HTTP server in this test. - This is a very clean way to test the “core route” independent of any transport (REST, JMS, etc.), while still using the real production code path.
- Setting the
nameheader matches how Rest DSL passes path parameters into the route, without needing a full HTTP roundtrip in this test. - Assertions check only the JSON payload, which is exactly what the route is responsible for producing.
This is textbook “unit test the route behind the REST layer.”
5. Unit test for the full REST endpoint over HTTP
This test exercises the full REST mapping via HTTP using Camel’s netty-http client URI.
package com.example;
import org.apache.camel.RoutesBuilder;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class RestRouteHttpTest extends CamelTestSupport {
@Override
protected RoutesBuilder createRouteBuilder() {
return new RestRoute();
}
@Test
void shouldReturnGreetingOverHttp() {
String response = template.requestBody(
"netty-http:http://localhost:8081/api/hello/Bob",
null,
String.class
);
assertEquals("{\"message\": \"Hello, Bob!\"}", response);
}
}
Rationale:
- Using
netty-http:http://localhost:8081/...calls the REST endpoint as an HTTP client would, validating path, verb, port, and basic JSON response. - This is integration-style, but still in‑JVM under
CamelTestSupport, so it is relatively cheap to run.
6. Patterns to remember (Camel 4–friendly)
A quick pattern table to keep the approach straight:
| Aspect | Pattern | Why it matters |
|---|---|---|
| Production RouteBuilder | Only real components (direct:, seda:, REST, …) |
Keeps production routes clean; no mock: leaks into deployed code. |
| Enabling advice | Apply AdviceWith in setup, then start context |
Replaces older flag-based patterns; explicit and compatible with modern test support. |
| Direct unit tests | direct: + MockEndpoint via advice |
Fast, in‑JVM tests of route logic with clear seams. |
| Async-style unit tests | seda: producer + mocked tail via advice |
Simulates real asynchronous flows while remaining isolated and observable. |
| REST business logic | Test direct: route behind REST |
Separates transport concerns from core logic, making tests clearer and refactors safer. |
| REST mapping correctness | HTTP calls via netty-http |
Validates URIs, verbs, port, and binding that pure route tests cannot see. |
The general rationale is:
- Design routes as you would for production, with real components only.
- Use
AdviceWithin tests (configured before starting the context) to splice inmock:endpoints where you need observability or isolation. - Layer tests: internal routes (
direct:/seda:) for behaviour; REST/HTTP tests for contracts and configuration.
Recent Comments