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.

  • CamelTestSupport manages the lifecycle of the CamelContext and exposes context, template (a ProducerTemplate), and getMockEndpoint.
  • You inject messages with template.sendBody(...) or template.requestBody(...) into direct:, seda:, or HTTP endpoints
  • You assert via:
    • MockEndpoint expectations (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:start is a synchronous, in‑JVM endpoint ideal as a “unit test entry point” and also usable in production wiring.
  • direct:result is a real internal endpoint you can “tap” from tests using AdviceWith, 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 in mock: via AdviceWith.
  • You apply advice in @BeforeEach while 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:inputseda:output) is unchanged.
  • The test uses AdviceWith to “cut off” the external consumer and replace it with mock: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 creates from("direct:hello"). This gives you a stable seam for unit tests: any test that calls configureHelloRoute() will have a valid direct:hello consumer.
  • The main configure() wires REST to that internal route, which is the transport layer. By keeping this wiring in configure() and the logic in configureHelloRoute(), 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:hello directly 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 only configureHelloRoute(), you intentionally skip restConfiguration() and rest("/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 name header 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 AdviceWith in tests (configured before starting the context) to splice in mock: endpoints where you need observability or isolation.
  • Layer tests: internal routes (direct:/seda:) for behaviour; REST/HTTP tests for contracts and configuration.