Cameleer

Testing routes: validate, cameleer test and Rust tests

Test Cameleer routes at three levels: cameleer validate checks route files without running them, cameleer test runs routes from a YAML test file and checks bodies, headers and mock endpoints without writing code, and Rust tests drive routes with a ProducerTemplate and assert on mock: endpoints, as Camel's Java tests do. All three run without the real Kafka, database or cloud service.

Applies to: YAML and XML routes (cameleer validate, cameleer test) and the Rust DSL (cargo test) ยท Apache Camel: Testing, Mock

levelcommandwhat it catchesneeds
staticcameleer validate routes/YAML and XML syntax, unknown EIPs and options, bad endpoint URIs, broken expressions, unresolved placeholders, dangling direct: endpoints, duplicate route idsnothing runs
behaviour, no codecameleer test routes/orders.test.yamlwhat a route does with a message: the body it returns, headers, which mock endpoints received what, errors; tool calls and approvals for AI routesthe routes run in-process
behaviour, in Rustcargo testeverything the YAML tests catch, plus any assertion you can write in Rusta Rust project

Validate every route file

cameleer validate loads route files the way cameleer run would, resolves every endpoint URI against the real components (an unknown option is an error, as in Camel), and starts nothing. It exits with 0 when everything is valid, 1 on errors and 2 on a usage error, so it fits a CI step or a pre-commit hook. --format json gives a report a coding agent can act on. The codes and the format are in Validate route files.

bash
cameleer validate routes/
cameleer validate routes/ --format json --strict    # warnings fail too

cameleer test: YAML tests without code

A test file (*.test.yaml) lists tests. Each test loads route files, sets properties, sends messages and checks what came back. cameleer run and cameleer validate skip test files, so they can live next to the routes.

A worked example

This route sends high-priority orders to one Kafka topic and the rest to another. The topics are placeholders with the real endpoint as the default, so a test can swap them for mock: endpoints without editing the route:

yaml example: testing-order-lanes
- route:
    id: order-lanes
    from:
      uri: direct:orders
      steps:
        - choice:
            when:
              - simple: "${body[priority]} == 'high'"
                steps:
                  - setHeader:
                      name: lane
                      constant: express
                  - to: "{{orders.express:kafka:orders-express?brokers=localhost:9092}}"
            otherwise:
              steps:
                - setHeader:
                    name: lane
                    constant: standard
                - to: "{{orders.standard:kafka:orders?brokers=localhost:9092}}"
        - setBody:
            simple: "accepted ${body[id]}"

The test sets the two placeholders to mock endpoints and sends two orders:

yaml
- test:
    name: a high-priority order takes the express lane
    routes: [orders.camel.yaml]
    properties:
      orders.express: mock:express
      orders.standard: mock:standard
    steps:
      - send:
          to: direct:orders
          body: {id: A1, priority: high}
        expect:
          body: accepted A1
          headers: {lane: express}
          mocks: {"mock:express": 1, "mock:standard": 0}
      - send:
          to: direct:orders
          body: {id: A2, priority: low}
        expect:
          body: accepted A2
          headers: {lane: standard}
          mocks: {"mock:express": 1, "mock:standard": 1}
bash
cameleer test orders.test.yaml
docker run --rm -v "$PWD:/routes:ro" pmsudhi/cameleer test /routes/orders.test.yaml

It prints PASS a high-priority order takes the express lane and 1 passed, 0 failed, and exits with 0; a failing check prints FAIL, the step number and what differed, and exits with 1. Mock counts add up over the steps of one test, and each test starts with fresh routes. This example runs in the test suite (testing_guide_worked_example in crates/cameleer-main/tests/docs_reference.rs).

The test file

keywhat it does
nameprinted with PASS or FAIL
routesroute files to load, relative to the test file (a list, or one file)
propertiesconfiguration keys and placeholder values for this test; a file: value is relative to the test file. application.properties is not read, so the test says everything it depends on
stepsrun in order; a step without expect fails if the route throws
stepwhat it does
sendto an endpoint, with body (text or any YAML value: a map arrives as an object, as JSON would) and headers; the body and headers at the end of the route are what expect checks
callToolcall an ai-tool through the tool envelope: name, arguments, callId, conversation, principal, approvedBy
approve, denydecide the oldest call of tool waiting for approval, with by and (for deny) reason
compensaterun the compensations registered in a conversation
expect keychecks
bodythe body equals this (objects and lists compared as JSON)
bodyContainsthe body contains this text
headerseach header has this value
errorthe route failed with a message containing this text
mockshow many messages each mock endpoint received so far
status, isError, duplicatetool results: the envelope's status, whether the result is an error, whether a repeated call returned the first result

callTool, approve, deny and compensate need the ai feature, which default builds have. For agent routes, the model's responses can be recorded once and replayed, so the test needs no model and no API key: see the agent step.

Replace real endpoints in a test

Two ways, neither edits the route under test:

  • Placeholders, as above: write the endpoint as {{orders.express:kafka:...}} and set orders.express: mock:express in the test's properties. Clear and explicit, and it also makes the endpoint configurable per environment.
  • An intercept file listed in the test's routes. Camel's interceptSendToEndpoint with skipSendToOriginalEndpoint catches every message sent to the matching endpoints and sends it to a mock instead:
yaml example: testing-intercept-kafka
- interceptSendToEndpoint:
    uri: "kafka:*"
    skipSendToOriginalEndpoint: true
    steps:
      - to: mock:kafka

With routes: [orders.camel.yaml, intercept-kafka.camel.yaml], the orders above reach mock:kafka and nothing connects to a broker. The intercept applies to every route file the test loads, in any order, as in Camel.

Rust tests

In Rust, a test builds a CamelContext, adds routes, sends messages with a ProducerTemplate and asserts on MockEndpoints, the same pattern as Camel's CamelTestSupport:

rust
use cameleer_core::prelude::*;

#[test]
fn high_priority_orders_take_the_express_lane() -> Result<()> {
    let ctx = CamelContext::new();
    ctx.add_routes(|b| {
        b.from("direct:orders")
            .choice()
            .when(header("priority").is_equal_to("high"))
            .to("mock:express")
            .otherwise()
            .to("mock:standard")
            .end();
    })?;
    ctx.start()?;

    let express = ctx.mock_endpoint("mock:express")?;
    express.expected_message_count(1);
    express.expected_bodies_received(vec!["A1"]);

    let template = ctx.create_producer_template();
    template.send_body_and_header("direct:orders", "A1", "priority", "high")?;
    template.send_body_and_header("direct:orders", "A2", "priority", "low")?;

    express.assert_is_satisfied()?;
    assert_eq!(ctx.mock_endpoint("mock:standard")?.received_counter(), 1);
    ctx.stop()
}

This test runs in the suite as written (testing_guide_rust_example, which also checks that this page shows the same code).

Sending. ProducerTemplate has Camel's methods with Rust casing: send_body, send_body_and_header, send_body_and_headers (one-way), request_body and request_body_and_header (request-reply, returns the reply body), request (build the exchange in a closure, get the whole reply exchange back), and async_request_body. ConsumerTemplate (ctx.create_consumer_template()) reads from a polling endpoint: receive_body("seda:out", timeout).

Mock expectations. expected_message_count, expected_minimum_message_count, expected_bodies_received (in order) and expected_bodies_received_in_any_order, expected_header_received, expected_property_received, expects_no_duplicate_bodies, message(i, predicate), all_messages(predicate) and expects(closure) set expectations; assert_is_satisfied() waits up to the result wait time (set_result_wait_time) for them and returns an error that says what differed, in Camel's wording. received_bodies(), received_exchanges() and wait_for_messages(n, timeout) read what arrived. A mock can also answer like the real service: return_reply_body(...), when_any_exchange_received(processor). Every option is on the mock component page.

YAML routes from Rust. To test YAML route files with Rust assertions, load them with cameleer_main::new_main(), set_property the placeholders to mock endpoints, then add_route_file(...) and start(), and use main.context() as above.

No real servers. Cameleer's own suite (2,800+ tests) runs hermetically: the components are tested against embedded servers written for the tests, never the network. For your routes, mock:, seda: and direct: endpoints stand in for the outside world, and placeholders switch endpoints between the test and production.

In CI

A job needs only the binary or the Docker image:

yaml
# .github/workflows/routes.yml
name: routes
on: [push, pull_request]
jobs:
  routes:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Validate route files
        run: docker run --rm -v "$PWD:/routes:ro" pmsudhi/cameleer validate /routes/routes
      - name: Run route tests
        run: docker run --rm -v "$PWD:/routes:ro" pmsudhi/cameleer test /routes/routes/orders.test.yaml

Both commands exit with a non-zero code on failure, which fails the job. Pin the image to a version tag (pmsudhi/cameleer:0.5.1) for repeatable builds.

Common questions

How do I test a route that calls Kafka, a database or a cloud service?

Replace the endpoint with a mock in the test, through a placeholder or an intercept file (see Replace real endpoints). The route itself stays as it runs in production.

Does Cameleer have Camel's adviceWith?

No. Use placeholders for the endpoints you want to swap, an interceptSendToEndpoint with skipSendToOriginalEndpoint for whole families of endpoints, or direct: starts that a test can send to directly.

Can I test a route that starts from a timer, a file or a queue?

Give the consumer a placeholder too (from: uri: "{{orders.in:file:inbox}}") and set it to direct:orders-in in the test, then send to that. Or keep the real consumer when it is local: a file: route can be tested by writing files into a temporary folder.

How do I test an AI agent without calling a model?

Record the model's responses once with record: file:... on the agent step or the model component, then replay them in cameleer test with replay: file:... (set through a test property); tool calls, approvals and compensation are checked with callTool, approve and compensate. See the agent step.

How do I check that my routes load before deploying?

cameleer validate in CI or a pre-commit hook; it starts nothing and needs no credentials. A coding agent can read its --format json output and fix what it reports.

Found a problem?

If something on this page doesn't match what Cameleer does, or you hit a bug while building with it, please help fix it, whether you are a person or a coding agent: open an issue that describes what you saw, then fork the repository, fix it, add or update a test, run cargo test --workspace, and open a pull request. CONTRIBUTING.md explains the ground rules.

Edit this page on GitHub