Testing routes: validate, cameleer test and Rust tests
Test Cameleer routes at three levels:
cameleer validatechecks route files without running them,cameleer testruns routes from a YAML test file and checks bodies, headers and mock endpoints without writing code, and Rust tests drive routes with aProducerTemplateand assert onmock: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
| level | command | what it catches | needs |
|---|---|---|---|
| static | cameleer validate routes/ | YAML and XML syntax, unknown EIPs and options, bad endpoint URIs, broken expressions, unresolved placeholders, dangling direct: endpoints, duplicate route ids | nothing runs |
| behaviour, no code | cameleer test routes/orders.test.yaml | what a route does with a message: the body it returns, headers, which mock endpoints received what, errors; tool calls and approvals for AI routes | the routes run in-process |
| behaviour, in Rust | cargo test | everything the YAML tests catch, plus any assertion you can write in Rust | a 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.
cameleer validate routes/
cameleer validate routes/ --format json --strict # warnings fail toocameleer 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:
- 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:
- 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}cameleer test orders.test.yaml
docker run --rm -v "$PWD:/routes:ro" pmsudhi/cameleer test /routes/orders.test.yamlIt 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
| key | what it does |
|---|---|
name | printed with PASS or FAIL |
routes | route files to load, relative to the test file (a list, or one file) |
properties | configuration 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 |
steps | run in order; a step without expect fails if the route throws |
| step | what it does |
|---|---|
send | to 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 |
callTool | call an ai-tool through the tool envelope: name, arguments, callId, conversation, principal, approvedBy |
approve, deny | decide the oldest call of tool waiting for approval, with by and (for deny) reason |
compensate | run the compensations registered in a conversation |
expect key | checks |
|---|---|
body | the body equals this (objects and lists compared as JSON) |
bodyContains | the body contains this text |
headers | each header has this value |
error | the route failed with a message containing this text |
mocks | how many messages each mock endpoint received so far |
status, isError, duplicate | tool 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 setorders.express: mock:expressin the test'sproperties. Clear and explicit, and it also makes the endpoint configurable per environment. - An intercept file listed in the test's
routes. Camel'sinterceptSendToEndpointwithskipSendToOriginalEndpointcatches every message sent to the matching endpoints and sends it to a mock instead:
- interceptSendToEndpoint:
uri: "kafka:*"
skipSendToOriginalEndpoint: true
steps:
- to: mock:kafkaWith 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:
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:
# .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.yamlBoth 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.
Related
- Validate route files, the cameleer command line, Configuration
- mock, dataset and test components
- The agent step: record and replay for agent routes
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.