Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

flexurio-integration-test

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:

  1. POST dummy data
  2. GET and compare the response against the dummy data that was POSTed
  3. PUT new data
  4. GET and compare again against the new data
  5. DELETE
  6. 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.

Table of Contents

Architecture

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.rsrunner::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.

Installing / Building

Requires a Rust toolchain (edition 2024).

cargo build --release

Running

cargo run -- examples/users_crud.json --base-url http://localhost:8080
  • First argument: path to the scenario JSON file.
  • --base-url (optional): overrides the config.base_url defined in the JSON file.

The exit code is 0 if every step passed, 1 if any step failed — suitable for use in CI.

Scenario JSON format

{
  "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"] }
    }
  ]
}

Step fields

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.

Multipart body

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 multipart is 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 directory cargo run is invoked from). Absolute paths are also supported.
    • file_name (optional): file name sent to the server; defaults to the file name from file.
    • content_type (optional): MIME type; defaults to application/octet-stream if not set.
  • body and multipart cannot both be set on the same step.
  • A compare.against_step pointing at a step with multipart can 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 1 POST with multipart (file upload), step 2 GET with no body, step 3 PUT with a JSON body, step 4 DELETE with no body — all valid within the same scenario, as shown in examples/multipart_upload.json below.

Full example (multipart upload → verify → update with JSON body → verify → delete): examples/multipart_upload.json.

Adding a scenario for another resource

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:8080

You 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).

Running this tool's own tests

cargo test

tests/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.

Project structure

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

About

Simple Integration testing for any REST API Project.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages