Testing
On this page
Philosophy
-
NEVER dismiss test failures as transient. Investigate root cause; classify as an edge case only after thorough analysis.
-
All checks must pass before push — enforced by the pre-push hook.
-
Test results are saved to
test-results/at the repo root — always check it for failure context.
Test Output
All test results go in test-results/ at the repo root. Non-negotiable — do not
mount, write, or look for results anywhere else.
test-results/ ├── unit/ # cargo-nextest unit test JUnit XML ├── integration/ # cargo-nextest integration test JUnit XML ├── e2e/ # Playwright JUnit XML + traces/screenshots └── ci/ # CI-only artifacts (coverage, SAST, container scan reports)
-
test-results/is gitignored — never committed. -
All test types produce JUnit XML in their subdirectory; CI consumes these exact paths.
-
When reviewing failures, read the full XML — do not
tail/head/partial-read.
Test Runner
-
cargo-nextest is the standard runner for all Rust tests (unit + integration).
-
Config
.config/nextest.toml; CI profile writes JUnit XML; all profilesfail-fast = false.
E2E Framework
-
Playwright is mandatory for all projects with a web UI — no Cypress, no Selenium.
-
E2E tests run inside Docker, never on the host.
-
Playwright config includes a JUnit reporter to
test-results/. -
Projects without a web UI delete the E2E sections entirely (no empty placeholders).
Pre-Push Hook
Location .githooks/pre-push; activate git config core.hooksPath .githooks &&
chmod +x .githooks/. The *sole functional-correctness gate — it runs the full
local battery (CI runs only security/supply-chain scans).
Step 1 — cargo xtask validate --skip-docker (all must pass): public-visibility
check; commit-signature verification; mandatory project docs exist; SPDX headers on
.rs; cargo fmt --check --all; cargo clippy --all-targets — -D warnings;
cargo nextest run --workspace --profile integration (120s + JUnit).
Step 2 — cargo xtask e2e: Docker build + containers + Playwright (when the
project has a web UI / docker-compose), then teardown.
Step 3 — cargo doc --workspace --no-deps: rustdoc compiles, with broken
intra-doc links denied (RUSTDOCFLAGS="-D rustdoc::broken_intra_doc_links") so a
broken link hard-blocks rather than only warning.
Step 4 — cargo xtask perf --profile smoke: k6 smoke when configured;
soft-skips (exit 4) when there is no load suite (opt out with SKIP_PERF=1).
What CI runs (it does NOT duplicate the battery)
CI is security + supply-chain + release only: cargo-audit/cargo-deny/
cargo-machete (audit-tools image); GitLab SAST + secret-detection; advisory
plan-lint, check-docs, secrets-yaml-lint, fn-shape-report,
audit-memory; release (tags only) sbom + release-xtask.
Nextest Profiles
| Profile | Use | Timeout | JUnit Output |
|---|---|---|---|
|
Local dev ( |
60s |
None |
|
Pre-push hook |
120s |
|
|
CI unit tests |
60s |
|
|
CI full devstack tests |
120s (8 threads) |
|
All profiles fail-fast = false.
Deterministic Seed-Based Test Data
-
Generators take a
--seedfor deterministic output (same seed = same entities, UUIDs, relationships). -
No seed → generate a random one and print it to stderr so failures reproduce.
-
E2E tests consume typed manifests generated from the seed, not hardcoded values.
-
Document the minimum seed size needed for pagination/boundary tests.
cargo run -p project-seed -- --seed 42 --families 12 # deterministic
cargo run -p project-seed # random; prints seed to stderr
Integration Test Guards
Infrastructure-dependent tests (databases, Docker, external services) must guard:
if !devstack_available().await { return; } // skip if infra isn't running
Never fail with "connection refused" — guard first (health endpoint, ~3s timeout). A skip is not a failure.
Test Harness Pattern
-
A
TestHarnessstruct manages authenticated HTTP clients, cleanup stacks, and lifecycle. -
Builder patterns for entities (
PersonBuilder,CaseBuilder) — don’t hand-build JSON. -
Typed service clients with bearer-token injection; cleanup tears down created entities on drop.
Performance Testing
-
Criterion (micro) — add benchmarks when performance is a stated requirement or a hot path is identified; >10% regression on a hot path warrants investigation. On-demand (
cargo bench), not in pre-push/CI by default. -
k6 (macro) —
cargo xtask perf; profilessmoke(1 VU ~5s, in pre-push),load,stress,soak; threshold assertions on p95 latency + error rate; results totest-results/k6/; runs in Docker (grafana/k6).
Test Categories (Taxonomy)
Name a test for the property it asserts, not the endpoint/function it calls.
| Category | Purpose | Location |
|---|---|---|
Functional |
Happy-path correctness |
|
Invariant |
Cross-cutting properties (uniqueness, referential integrity, drift) |
|
Concurrency |
Races, deadlocks, lock ordering, parallel safety |
|
Fault-injection |
Behavior under failures (network/broker/partial-write) |
|
Recovery |
Behavior after failure (retry, idempotency, rollback, replay) |
|
Contract |
API/wire-format pinning (response shapes, error schemas, CSP) |
|
Property-based |
Universally-quantified properties via |
|
Mutation |
Uncovered-path detection via |
driven by |
Process rule: concurrency / fault-injection / platform-invariant fixes MUST ship with a test in the corresponding category — a race-condition bug gets a concurrency test, not just a functional one.
Constraint Tests Are Hand-Curated
API-boundary rejection tests (UNIQUE/FK violations, enum validation, duplicate-create) are hand-written, not auto-derived from SQL schemas or OpenAPI. Auto-generation produces shallow tests that miss the real boundary cases — write the test for the rejection behavior you want.
Invariant Tests with Drift Contract
For projects with rulesets, migrations, or per-service catalogs, maintain an
invariant test that sweeps the catalog at test time: every file parses, names are
unique, constants match the code. Examples: all migrations apply against a fresh
DB; all rulesets/*.json names unique; rulesets reference only code-defined enums.
Evil Input Corpus
For services accepting user-controlled input, maintain a hostile-input corpus per category, run via a parametric macro (adding an endpoint becomes a one-line subscription):
| Category | Example payloads |
|---|---|
|
Tampered signature, expired, unsigned |
|
Zip-bomb, polyglot, MIME mismatch, path-traversal filename |
|
NULL bytes, oversized (10MB), homoglyph, RTL override, control chars |
|
Wrong version, zero UUID, non-canonical, oversized |
|
Deeply nested, duplicate keys, integer overflow, NaN/Infinity |
|
|
|
XSS variants, mXSS, SVG-embedded JS, data: URLs, on-attribute handlers |
|
Bidi override, zero-width joiners, normalization mismatches |
|
Unknown variant, case-mismatch, oversized, wrong type |
|
Pre-epoch, year 9999, leap second, timezone gap, non-ISO |
|
Boundary in payload, missing boundary, oversized/malformed part |
|
Truncated, wrong algorithm, key confusion (HS256 vs RS256), replay |
Each evil-input test asserts: rejected with 4xx, no PII in the error, no internal state mutation, no log spam.
Property-Based Testing
Property-based testing (proptest / quickcheck) is mandatory for parsers,
deserializers, serializers, and numerical/math logic, and anything with
universally-quantified invariants. State the invariant; let the tool find
counter-examples. Example-based tests with hand-picked values miss edge cases
(empty/max-size inputs, surrogate pairs, overflows). Prefer proptest (better
shrinking); ProptestConfig { cases: 1000, .. } for slow targets.
Coverage Floor Gate
cargo xtask coverage [--threshold N] [--baseline] wraps cargo-llvm-cov (LCOV
JSON to test-results/coverage/), compares against .coverage-baseline.json, and
fails on a >0.5% drop. --baseline refreshes; --threshold N enforces a minimum
line coverage (CI-gateable). Tracking baselines prevents silent test decay.
Mutation Testing
cargo xtask mutants [--smoke] wraps cargo-mutants to find uncovered paths.
--smoke runs a 1/20 shard (~minutes); full runs in scheduled CI. Accepted mutants
are documented in mutants-baseline.toml with per-entry justification.
Critical Rules
-
Never dismiss test failures as transient — investigate root cause.
-
All checks must pass before push — fmt, clippy, tests (hook-enforced).
-
Test results in
test-results/— check it before asking questions. -
Docker-based tests run in Docker — never run Playwright/E2E on the host.
-
Never weaken a test to make code pass — fix the code, not the test. No delete/skip/
#[ignore]; no loosened assertions; no changed expected values. The only legitimate test edit is a genuinely-incorrect test, explained in the commit. -
All test types produce JUnit XML to
test-results/.
Project-specific test types, commands, the CI pipeline, and E2E setup live in the project’s project testing page.