A Rust CLI that runs HTTP integration tests against a REST API, where the sequence of test steps is defined in a JSON file instead of being hardcoded in Rust.
The core idea: a test scenario is just data. You write an ordered list of HTTP requests (method, endpoint, body, expected status) in JSON, and the runner executes them in that exact order, substituting captured values between steps and comparing responses along the way. Changing the test flow means editing JSON — no recompilation, no touching Rust code.
A typical CRUD scenario looks like this:
- POST dummy data
- GET and compare the response against the dummy data that was POSTed
- PUT new data
- GET and compare again against the new data
- DELETE
- GET to confirm the resource is gone (expect
404)
But the runner does not hardcode this POST→GET→PUT→GET→DELETE→GET flow —
it simply executes steps in array order, so any method in any order is
valid (e.g. GET → POST → GET → PATCH). The CRUD pattern above is just the
most common usage, not a built-in constraint.
- Architecture
- Installing / Building
- Running
- Scenario JSON format
- Multipart body
- Adding a scenario for another resource
- Running this tool's own tests
The project is a small binary crate (src/main.rs) built on top of a
library crate (src/lib.rs) that exposes seven focused modules. Each module
has a single responsibility, and runner is the only module that ties them
together:
| Module | File | Responsibility |
|---|---|---|
scenario |
src/scenario.rs |
Defines the JSON data model: Scenario, Config, Step, HttpMethod, CompareSpec, MultipartField. This is what serde_json deserializes the scenario file into. |
context |
src/context.rs |
Holds per-run state: variables captured from responses ({name: value}) and the request body sent by each step (keyed by step name), so later steps can reference earlier ones. Also provides extract_dot_path for pulling a nested field out of a JSON response (e.g. "data.id"). |
template |
src/template.rs |
Substitutes {{var_name}} placeholders — in endpoint strings and recursively through JSON request bodies — with values looked up in the Context. Errors if a referenced variable was never captured. |
http_client |
src/http_client.rs |
Executes a single HTTP request via reqwest, sending either a JSON body or a multipart/form-data body (reading files from disk for file fields), and returns the status code plus parsed JSON response body (if any). |
compare |
src/compare.rs |
Compares a chosen set of fields between two JSON values (partial_match), ignoring any other fields. Used to check a GET response against the body a previous step sent, without failing on server-added fields like id or created_at. |
runner |
src/runner.rs |
Orchestrates the whole run: iterates scenario.steps in order, and for each step — substitutes templates, resolves multipart file paths, sends the request via http_client, records the request body and captured variables in Context, and runs compare if the step declares one. A failing step does not stop the run, so every step gets executed and reported. |
report |
src/report.rs |
Prints a colored PASS/FAIL line per step (with status mismatch and field mismatch detail) plus a final summary, and returns whether every step passed. |
Execution flow (main.rs → runner::run):
CLI args (scenario file, --base-url)
│
▼
Read & parse scenario JSON ───────────► scenario::Scenario
│
▼
runner::run (loop over scenario.steps, in file order)
for each step:
1. template::substitute_str/substitute_value — resolve {{vars}} in endpoint/body
2. resolve multipart file paths (relative to the scenario file's directory)
3. http_client::execute — send the HTTP request
4. context::Context::record_request_body — remember what was sent
5. context::extract_dot_path per `capture` entry — save values for later steps
6. compare::partial_match, if `compare` is set — check fields against a prior step
│
▼
report::print (PASS/FAIL per step + summary) ──► process exit code (0 or 1)
Because Context is threaded through the whole loop, step N can reference
any variable captured by steps 1..N-1 via {{var_name}}, and can compare
its response against the request body recorded by any earlier step.
Requires a Rust toolchain (edition 2024).
cargo build --releasecargo run -- examples/users_crud.json --base-url http://localhost:8080- First argument: path to the scenario JSON file.
--base-url(optional): overrides theconfig.base_urldefined in the JSON file.
The exit code is 0 if every step passed, 1 if any step failed — suitable
for use in CI.
{
"config": {
"base_url": "http://localhost:8080",
"headers": { "Authorization": "Bearer xxx" }
},
"steps": [
{
"name": "create_user",
"method": "POST",
"endpoint": "/users",
"body": { "name": "John Doe", "email": "john@example.com" },
"expect_status": 201,
"capture": { "user_id": "id" }
},
{
"name": "verify_create",
"method": "GET",
"endpoint": "/users/{{user_id}}",
"expect_status": 200,
"compare": { "against_step": "create_user", "fields": ["name", "email"] }
}
]
}| Field | Required | Description |
|---|---|---|
name |
yes | Unique step name, used as a reference in other steps' capture/compare |
method |
yes | GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS |
endpoint |
yes | Path relative to base_url; may contain {{var}} placeholders |
body |
no | JSON request body (for POST/PUT/PATCH); string values may contain {{var}}. Mutually exclusive with multipart |
multipart |
no | multipart/form-data body. Mutually exclusive with body — see Multipart body |
expect_status |
yes | Expected HTTP status code |
capture |
no | { "var_name": "dot.path.in.response" } — extracts a value from the response body and stores it for later steps to use via {{var_name}} |
compare |
no | { "against_step": "<other_step_name>", "fields": [...] } — compares specific fields of this step's response against the body sent in another step (e.g. the dummy data that was POSTed) |
compare only checks the fields listed in fields, so extra
server-generated fields (id, created_at, etc.) don't affect the result.
method is not constrained to any fixed order or combination — every
step can use any method in any order (e.g. GET → POST → GET → PATCH is
also valid), because the runner strictly follows the steps array order; it
does not hardcode a POST→GET→PUT→GET→DELETE→GET flow. The CRUD example above
is just one common usage pattern.
For steps that need to upload a file (e.g. POST a document or image), use
multipart instead of body:
{
"name": "upload_document",
"method": "POST",
"endpoint": "/documents",
"multipart": {
"caption": "invoice scan",
"file": {
"file": "./fixtures/dummy.txt",
"content_type": "text/plain"
}
},
"expect_status": 201,
"capture": { "document_id": "id" }
}- Each key in
multipartis a form field name. - A string value → sent as a plain text field (may contain
{{var}}). - An object value
{ "file": "...", "file_name": "...", "content_type": "..." }→ sent as a file field:file(required): path to the file, relative to the location of this scenario JSON file (not relative to the directorycargo runis invoked from). Absolute paths are also supported.file_name(optional): file name sent to the server; defaults to the file name fromfile.content_type(optional): MIME type; defaults toapplication/octet-streamif not set.
bodyandmultipartcannot both be set on the same step.- A
compare.against_steppointing at a step withmultipartcan only compare its text fields (file fields have no JSON representation to compare against). body/multipart/ no body at all can be freely mixed within a single JSON file — they're per-step fields, not a global setting. E.g. step 1POSTwithmultipart(file upload), step 2GETwith no body, step 3PUTwith a JSONbody, step 4DELETEwith no body — all valid within the same scenario, as shown inexamples/multipart_upload.jsonbelow.
Full example (multipart upload → verify → update with JSON body → verify → delete):
examples/multipart_upload.json.
Duplicate the pattern in examples/users_crud.json, change the
endpoint/body/fields to match the resource you want to test, then run:
cargo run -- examples/my_resource_crud.json --base-url http://localhost:8080You can create as many JSON files as you need — one file per resource, or a
single file covering several resources at once (step order always follows
the steps array order).
cargo testtests/runner_integration.rs runs the runner against a mock HTTP server
(httpmock) to verify that templating, capturing, and comparing all work
correctly, without needing an external API or database.
src/
main.rs CLI entry point: parses args, reads/parses the scenario file, runs it, prints the report
lib.rs Re-exports the library modules
scenario.rs JSON data model (Scenario, Config, Step, HttpMethod, ...)
context.rs Captured variables + recorded request bodies, dot-path extraction
template.rs {{var}} substitution in strings and JSON values
http_client.rs Sends a single HTTP request (JSON or multipart)
compare.rs Partial field comparison between two JSON values
runner.rs Orchestrates step execution in scenario order
report.rs Colored console PASS/FAIL report
examples/ Example scenario JSON files and fixture files they reference
tests/ Integration tests against a mock HTTP server