camel-integration-test 0.45.0

Scenario document model, parser, and integration-tier test harness for rust-camel
Documentation

camel-integration-test

Scenario-tier test support for rust-camel: .test.yaml documents that declare a scenario: section

Owns the scenario document model and parser behind the integration tier of camel test (ADR-0069). A scenario document runs one action vocabulary: send, receive, sleep, and validate. A send takes an optional method HTTP token, uppercased at load. An invalid token fails doc validation with exit 2. A send with a body defaults to POST. A bodyless send defaults to GET. A document may declare a top-level sendDeadline field. It is a humantime duration that bounds every send action. It defaults to 30 s. An invalid value fails load with a doc-validation error naming sendDeadline. The bound uses real time only; there is no virtual time. A send to a direct: route may declare expectReply. The matcher verbs are the same as validate: equals, regex, contains, startsWith, endsWith, exists, jsonSubset. It asserts the synchronous reply body. An expectReply mismatch is a verdict-class failure naming the expectation and the actual body. expectReply on any other target, an http: partner or fake:, is a load-time error. Runnable reference: tests/direct_reply_test.rs. The parser bans the unit-tier keys (inputs, expects, intercepts) at load time, and it rejects env keys that collide with a declared bindVar. The harness provisions each http: partner on 127.0.0.1:0 and folds the partner's bindVar into a layered environment. Config and route ${env:} placeholders resolve through that environment, never the process environment. Every action prints a PASS or FAIL row. Exit codes follow the ADR-0069 taxonomy: 0 when all actions pass, 1 on a verdict failure, 2 on a parse, boot, or apparatus failure.

Usage

A minimal scenario document (orders.test.yaml):

routeFiles:
  - routes/bridge.yaml
scenario:
  - send:
      to: direct:start
      method: PUT
      body: order-payload-7f3a
  - receive:
      from:
        endpoint: http://127.0.0.1:0/orders
        provisioning: harness
        bindVar: PARTNER
      deadline: 2s
      extract:
        body: body
  - validate:
      target: { lastReceived: http://127.0.0.1:0/orders }
      expectation: order-payload-7f3a

The partner endpoint binds on the harness and injects PARTNER=http://127.0.0.1:<bound> into the layered environment, so the route's ${env:PARTNER} reaches the local listener. Run the document with the CLI; the http: partner adapter rides the integration-http Cargo feature, default-on in camel-cli since 2026-09-05:

cargo run -p camel-cli -- test --integration orders.test.yaml

partners: section

A partners: section scripts how a harness partner answers requests. It is a map. Each key is the exact declared endpoint string, the :0 URI as written in the scenario. The value is a sequence of script entries.

Each entry carries optional method and path matchers. A request matches an entry when its method and path match. An entry with no method or path matches any request.

An entry declares exactly one of response or fault: close. The response holds optional status (100-599), headers, and body. The fault: close drops the connection without an HTTP response. The request is still recorded before the fault.

An entry may also declare times and delay. The times is an integer of 1 or more. It defaults to 1. An entry with no times keeps the shipped serve-once behavior. The entry serves its first times matching requests, then it is spent. The delay is a humantime string. The harness holds for the delay before it serves the response or commits the fault.

The harness serves the first unspent matching entry in order. A request no entry matches serves status 500 with an empty body. A document with no partners: section is permissive: every request gets status 200 with an empty body.

For example, a timed delayed response entry can precede a fault entry for the same request:

partners:
  http://127.0.0.1:0/orders:
  - method: PUT
    path: /orders
    times: 2
    delay: 100ms
    response:
      status: 201
      body: A
  - method: PUT
    path: /orders
    fault: close

The first entry serves the first two matching requests. Each response arrives after the delay with status 201 and body A. The entry is then spent. The third matching request hits the fault entry. Its connection drops without an HTTP response.

Every partners: key must equal a declared harness http endpoint reference. A key that matches no wired reference fails load with a doc-validation error, exit 2, naming the key. The check runs before any partner binds. A typo of a real key, for example http://127.0.0.1:0/order for :0/orders, fails here. It never falls silently to permissive.

validate targets

A validate action asserts against one of three targets, chosen by the single key of its target map:

  • lastReceived: the last message a receive action collected on that endpoint. The expectation is a subset match, as in the usage example above.
  • variable: a scenario variable set by an earlier extract. Variable existence is checked at run time. The expectation is a subset match against the variable's value.
  • partner: the recorded-request count of a harness partner, described next.

A partner target asserts the recorded-request count of a harness partner. The expectation holds count, an exact non-negative integer, plus optional method and path filters. The method filter compares ASCII-case-insensitively. The path filter compares the path-and-query exactly. The count counts only the recorded requests that pass both filters.

Without a deadline, the assertion reads one immediate snapshot. With a deadline, it polls a fresh snapshot every 100 ms until the filtered count equals the expectation or the deadline passes; the final snapshot then decides. Arrivals only add, so a count above the expectation fails at every snapshot and never settles back.

Partner expectations are exact-count, not subset like message expectations: the count must equal the filtered arrivals, never a lower bound.

Concurrency: the burst-send recipe

Back-to-back send: actions with no intervening receive: dispatch genuinely concurrent requests. Each send returns at connect; the request writes land asynchronously. The partner recorder is the proof surface: a validate with a partner target counts the wire arrivals.

The recipe sends a burst to one lane key, settles, then asserts the count. It mirrors the IMMEDIATE_COUNT_DOC shape:

scenario:
- send:
    to:
      endpoint: http://127.0.0.1:0/orders
      provisioning: harness
    method: POST
- send:
    to:
      endpoint: http://127.0.0.1:0/orders
      provisioning: harness
    method: POST
- send:
    to:
      endpoint: http://127.0.0.1:0/orders
      provisioning: harness
    method: POST
- sleep:
    duration: 100ms
- validate:
    target: {partner: http://127.0.0.1:0/orders}
    expectation: {count: 3}

The three sends park three responses on the same lane key. The sleep lets the writes land. The validate asserts the exact count of three.

Runnable references: tests/partner_verification_test.rs::immediate_count_assert_e2e and tests/http_partner_scripting_test.rs.

The v1 bound: same-key responses park in a bounded FIFO of 64. A burst past the bound fails apparatus-class, naming the lane key. The recipe has no native wall-clock concurrency-comparison primitive; that is a future consideration.

${name} interpolation

Scenario strings interpolate ${name}. The surface covers three places:

  • endpoint strings in send and receive,
  • body string leaves,
  • header values.

Substitution is string-only. A string leaf with no placeholder stays as is. Raw substitution applies, with no percent-encoding. An unset variable at send time fails scenario-var-unresolved, exit 2, naming the variable. Exit 2 is the apparatus class: an unset variable is an authoring bug, so CI reports a harness/document error, not a product failure.

$${ escapes a literal ${. The escape applies to body leaves and header values too. For example, a JSON body leaf that must reach the wire as ${literal} is written $${literal}.

The receive resolves by the interpolated authority. The path and query need not match the send. A receive declared as http://${PARTNER}/orders finds the roundtrip a map-form send parked.

Minimal route source

An integration document may declare routeFiles: []. The empty list is the minimal route source: no routes boot, and the scenario drives only harness partners.

Two layers, one name: PARTNER

The same name can carry two forms in one run.

Layer Form Usage
scenario variable host:port http://${PARTNER}/orders
route env http://host:port ${env:PARTNER}

One-line rule: scenario = authority, route env = full URI. ${env:} deliberately does not resolve in scenario strings.

The multi-path pattern builds on this rule: one declared partner and one bindVar serve every path a route dials on that authority (runnable pair: examples/integration-testing/partner-multi-path.test.yaml

  • partner-multi-path.routes.yaml).

inbound: section

A document may declare inbound: {bindVar: NAME}. The harness provisions a staged port-0 listener, and route files interpolate the full-URL variable through ${env:NAME}, for example from: ${env:NAME}/in. The bound address is exposed on the run outcome as inbound_bound. Documents that pin a literal port keep working — the back-compat shape that predates inbound:. v1 bound: boot-owning library callers only; the CLI cannot run inbound documents (named infra-unavailable). Runnable reference: tests/http_inbound_test.rs.

The Testing chapter documents the full action grammar, the partner adapters, and the exit contract. examples/integration-testing/ is a runnable example. The partner-retry-route.test.yaml pair there runs a real retrying route against a faulted partner and asserts the two wire attempts.

Capability matrix: what the tier can drive today

The tier is black-box at the HTTP boundary only (rc-xnob). Route shapes and their coverage:

route shape trigger today effect observable today
from:http -> to:http YES scenario client role YES partner + receive (v1 flagship)
from:http -> to:kafka/to:sql YES scenario NO consumer/probe exists
from:kafka -> to:http NO producer exists YES partner (matchers + lastReceived already free)
from:sql -> to:http NO seeding exists YES partner

The two gaps are independent capabilities, each unlocking half of the matrix: a non-HTTP trigger (partner-as-producer for kafka, or sql seeding) unlocks the bottom rows; a non-HTTP effect (a kafka consumer partner or an sql probe action) unlocks the right column. The extension points already exist in the contract — the Provisioning enum for partner transports and the scenario action enum for a future probe action (query -> extract -> validate reuses the existing variables and matchers). Open design questions for that horizon: partner lifecycle for long-lived transports (topics vs listeners), seeding ownership (harness vs app fixtures), and CI infrastructure (kafka container).

Related crates

  • camel-cli: camel test, which parses and runs the documents
  • camel-bundles: the boot cascade the scenario boot composes (ADR-0069 section 10)
  • camel-test: unit-tier harness; the scenario tier never depends on it (ADR-0055)