/testingdetermines the assertion type, execution level, and exception path before implementation/standardizing-rustis loaded before this reference- co-located spec tests use
<subject>.<evidence>.<level>[.<runner>].rsor the repo-local overlay - doubles preserve coupling to the real trait, function, protocol, or binary seam
- property assertions use meaningful
proptestorquickcheckproperties - compile-time claims use compile-fail evidence
- shared harnesses, generators, and fixtures live in a separate workspace-member crate (test-infrastructure production code), per the product's
test-infrastructurePDR - coverage claims are measured with the repository's real coverage tool or recorded as unavailable
spx/.../tests/<subject>.<evidence>.<level>[.<runner>].rs
Evidence tokens — the assertion type from /testing:
| Token | Assertion type | Meaning |
|---|---|---|
scenario |
Scenario | Concrete inputs and outputs through the governed function, module, or binary |
mapping |
Mapping | Table-driven or parameterized cases over a finite input/output mapping |
conformance |
Conformance | Parser, schema, protocol, CLI contract, or trybuild compile-time check |
property |
Property | proptest or quickcheck invariant over a generated domain |
compliance |
Compliance | Violating fixture, lint harness, or architecture review marker |
Level tokens — the infrastructure required to run the test:
| Token | Level | Infrastructure |
|---|---|---|
l1 |
1 | Rust stdlib, cargo test, temp dirs, repo-required dev tools |
l2 |
2 | Workspace binaries, local services, Docker, databases, local browser harnesses |
l3 |
3 | External network, deployed systems, SaaS APIs, browser UI, shared environments |
Optional runner token — appended after the level token when the test requires a specific async executor or test harness (e.g., tokio, actix).
Examples:
spx/55-example.enabler/21-auth.outcome/tests/session_token.scenario.l1.rs
spx/55-example.enabler/21-auth.outcome/tests/registry_fetch.conformance.l2.rs
spx/55-example.enabler/21-auth.outcome/tests/login_flow.scenario.l3.rs
Product specs or decisions may restrict which levels the product supports. Follow the repo-local Rust test overlay when it points to the governing spec or decision.
| Level | Infrastructure | Typical mechanisms |
|---|---|---|
| 1 | Rust stdlib, cargo test, temp dirs, repo-required dev tools |
#[test], pure functions, trait seams, tempfile, rstest |
| 2 | Workspace binaries, local services, Docker, databases, local browser harnesses | assert_cmd, real adapters, tokio::test, testcontainers |
| 3 | External network, deployed systems, SaaS APIs, browser UI, shared environments | live API probes, browser automation, deployed CLI/API workflows |
Level rules:
- Pure computation, parsing, serialization, config loading, command building, and cheap temp-dir filesystem behavior belong at Level 1
- Real workspace binaries, local DBs, local queues, local HTTP services, and containerized collaborators belong at Level 2
- Remote APIs, deployed systems, SaaS collaborators, browser UI, and shared environments belong at Level 3
- Product specs or decisions may disable Level 3 when the suite cannot safely stand up or isolate those collaborators; repo-local overlays only route skills to those declarations
| Router Decision | Rust implementation |
|---|---|
| Stage 2 -> Level 1 | #[test], pure functions, tempfile, hand-written trait impls |
| Stage 2 -> Level 2 | assert_cmd, real local adapters, tokio::test, local services |
| Stage 2 -> Level 3 | live API probes, deployed workflow tests, browser automation, remote contract checks |
| Stage 3A: pure computation | direct function tests with structural assertions |
| Stage 3B: extract pure part | pure helper at Level 1, boundary at the outer level |
| Stage 5 exception 1: failure simulation | trait impl returning deterministic errors |
| Stage 5 exception 2: interaction protocols | recorder struct capturing calls |
| Stage 5 exception 3: time/concurrency | injected clock, paused runtime time, deterministic channels |
| Stage 5 exception 4: safety | recorder or no-op implementation preserving the seam |
| Stage 5 exception 5: combinatorial cost | configurable in-memory implementation with real-shaped behavior |
| Stage 5 exception 6: observability | capture struct for spans, logs, events, or serialized output |
| Stage 5 exception 7: contract probes | local stub validated against the same contract schema |
| compile-time contract | trybuild |
| universal invariant | proptest or quickcheck |
Preferred controlled implementations:
- small hand-written structs implementing the same trait
- recorder structs that capture inputs for later assertions
- deterministic closures passed into function-based seams
- local harness services that exercise the real protocol
Reject by default:
mockall,faux, or generated mocks as the primary strategy- spying on the function or method under test instead of exercising it
- replacing the module under test with a fake implementation
If /testing reaches a Stage 5 exception, the double must still preserve coupling to the real interface or protocol. The exception explains why a controlled implementation is needed; it does not justify severing the seam.
| Need | Preferred tooling |
|---|---|
| L1 scenario and mapping tests | #[test], assert_eq!, rstest when parameterization helps |
| temp files or dirs | tempfile |
| async tests | #[tokio::test] or runtime-specific test macro |
| property testing | proptest or quickcheck |
| CLI binaries | assert_cmd and predicates |
| textual golden output | insta when the output surface itself is the assertion |
| compile-fail or diagnostics | trybuild |
| local services or containers | testcontainers or repo-native harnesses |
| coverage | cargo llvm-cov when available |
Snapshot tests are valid only when the textual or structured output surface is itself the contract. They are weak evidence for business logic that has a stronger structural assertion available.
Every value in a test has exactly one valid origin. Run through this table for each test value before writing it.
| Origin | What it means | Where it lives |
|---|---|---|
| Source-owned | The production module defines and exports the value | Import from that module |
| Generator-produced | Pure code emits varied values each run | test-owned generator module |
| Harness-managed | Infrastructure mediates interaction with an external resource | test-owned harness module |
| Descriptive inline | Human-readable text in the test name or assertion message | Inline in the test file |
THERE ARE NO VALID TEST-OWNED CONSTANTS. A named constant in a test file that duplicates a value the production module should own means the production code needs refactoring.
1. Source-owned values
ALWAYS import command names, rule names, matcher tokens, status values, domain identifiers, and public constants from the owning production module. If the module does not export them yet, refactor it to export them before writing the test.
// ❌ rejected: duplicates a value the production module should own
const PASS_STATUS: &str = "pass";
// ✅ preferred: import from the production module
use product::audit::GateStatus;
let status = GateStatus::Pass;
2. Generator-produced values
Use generators for inputs that vary per run. A generator is a pure function — it emits values, holds no state, and has no side effects.
- Use
proptestorquickcheckstrategies for randomized inputs - Write strategy factories for domain-shaped values
// tests/generators/audit.rs
fn valid_gate_statuses() -> impl Strategy<Value = GateStatus> {
prop_oneof![
Just(GateStatus::Pass),
Just(GateStatus::Fail),
Just(GateStatus::Skipped),
]
}
3. Harness-managed
Use harnesses for tests that interact with external systems — filesystems, APIs, binaries, testcontainers. A harness manages setup and teardown; it is not self-contained.
// product-testing/src/harnesses/spec_tree.rs
pub struct TestEnv {
pub root: tempfile::TempDir,
}
impl TestEnv {
pub fn new() -> Self {
TestEnv { root: tempfile::tempdir().expect("temp dir") }
}
}
Consumers depend on the workspace-member crate via [dev-dependencies] and import as use product_testing::harnesses::spec_tree::TestEnv;.
4. Fixture files
Use fixture files for real-world data the code under test would encounter: a captured JSONL from a chat session, a saved API response, a document the parser must handle. Fixture files live in the product-testing/ workspace-member crate under product-testing/fixtures/ and are read from disk by path — never compiled in or imported as modules. This is the cross-language test-infrastructure rule.
Strings and numbers are never valid fixtures. A string literal representing a domain value belongs in the production module or a generator, not a static file.
5. Test infrastructure layout
Harnesses, generators, and inert fixtures are production code. They live in a separate workspace-member crate (product-testing/ directory at workspace root, Cargo package product-testing, Rust import path product_testing), declared as a [dev-dependencies] entry of consumers:
product-testing/src/harnesses/<name>.rs— modules that mediate access to external resources.product-testing/src/generators/<name>.rs— factories producing valid inputs for proptest/quickcheck/parameterized tests.product-testing/src/fixtures/<name>.rs— fixture-loading code that reads inert data files by path.product-testing/fixtures/(data subdirectory) — inert input files.
Co-located helper modules are acceptable only when the helper serves a single test file. Anything shared across two or more test files belongs in product-testing/. Never use tests/support/, crate::test_support, super::tests, or #[cfg(test)] mod patterns as homes for shared test infrastructure — those keep ungoverned utility code inside production crates or under tests/.
- Do not read production source files as test input to prove behavior
Checked-in Rust script or helper binary entrypoints get thin tests:
- argument parsing through the repository's canonical parser
- dispatch into the imported orchestrator
- exit-code mapping and observable terminal output
The orchestrator carries the main behavioral evidence. Entry files stay small and route to tested modules.
| Assertion Type | Rust testing shape |
|---|---|
| Scenario | example-based tests with concrete inputs and outputs |
| Mapping | table-driven tests or rstest case matrices |
| Property | proptest or quickcheck with meaningful invariants |
| Conformance | validator tooling, parsers, schema checks, trybuild if compile-time |
| Compliance | targeted assertions, lint harnesses, or architecture review markers |
Property claims about parsers, serializers, math, ordering, or invariants require property-based tests unless the spec itself narrows the claim to a finite example set.
Compile-time contracts such as trait bounds, derive behavior, or diagnostic guarantees require trybuild or equivalent compile-fail evidence.
Pure function example:
#[test]
fn rejects_empty_url_sets() {
let result = validate_config(ConfigInput {
url_sets: BTreeMap::new(),
});
assert!(result.is_err());
}
Dependency seam example:
trait CommandRunner {
fn run(&self, program: &str, args: &[&str]) -> Result<CommandOutput, CommandError>;
}
struct SuccessRunner;
impl CommandRunner for SuccessRunner {
fn run(&self, _: &str, _: &[&str]) -> Result<CommandOutput, CommandError> {
Ok(CommandOutput::success("done"))
}
}
#[test]
fn command_builder_reports_success() {
let runner = SuccessRunner;
let result = sync_repo(Path::new("/src"), "origin", &runner).unwrap();
assert!(result.success);
}
Tempdir example:
#[test]
fn loads_yaml_from_temp_dir() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.yaml");
std::fs::write(&path, "site_dir: ./site\n").unwrap();
let config = load_config(&path).unwrap();
assert_eq!(config.site_dir, PathBuf::from("./site"));
}
proptest! {
#[test]
fn config_roundtrips(input in valid_config_strategy()) {
let encoded = encode_config(&input).unwrap();
let decoded = decode_config(&encoded).unwrap();
prop_assert_eq!(decoded, input);
}
}
Use trybuild for compile-time guarantees:
#[test]
fn ui_contracts_hold() {
let cases = trybuild::TestCases::new();
cases.pass("tests/ui/valid_builder.rs");
cases.compile_fail("tests/ui/missing_required_field.rs");
}
CLI binary example:
#[test]
fn init_command_writes_project_files() {
let temp = tempfile::tempdir().unwrap();
assert_cmd::Command::cargo_bin("herder")
.unwrap()
.current_dir(temp.path())
.args(["init", "demo"])
.assert()
.success();
assert!(temp.path().join("demo/Cargo.toml").exists());
}
Async L2 example:
#[tokio::test]
async fn repository_persists_and_loads_user() {
let db = test_database().await;
let repo = UserRepository::new(db.pool());
repo.save(&user_fixture()).await.unwrap();
let loaded = repo.find(UserId::new(1)).await.unwrap();
assert_eq!(loaded.email(), "user@example.com");
}
Remote API example:
#[tokio::test]
#[ignore = "requires credentialed sandbox"]
async fn published_package_is_fetchable_from_registry() {
let client = registry_client_from_env().unwrap();
let package = client.fetch_package("example-package").await.unwrap();
assert_eq!(package.name, "example-package");
}
Browser workflow example:
#[tokio::test]
#[ignore = "requires browser service and deployed app"]
async fn login_flow_reaches_dashboard() {
let browser = browser_session_from_env().await.unwrap();
browser.goto("/login").await.unwrap();
browser.fill("#email", "user@example.com").await.unwrap();
browser.click("button[type=submit]").await.unwrap();
assert!(browser.text("main").await.unwrap().contains("Dashboard"));
}
Level 3 tests must declare their isolation boundary, credentials, cleanup behavior, and expected runtime. If the repository has no safe Level 3 lane, stop and surface that product decision rather than hiding the dependency behind a skipped test.
- Prefer
cargo llvm-covfor per-file and per-function coverage deltas - Compare baseline coverage without the test under audit against coverage with the test included
- Report the actual delta; do not infer coverage from reading the file
- If the repository lacks usable coverage tooling, state that limitation explicitly and do not fabricate a coverage pass
| Anti-pattern | Why it fails |
|---|---|
| generated mocks for the main seam | severs evidence from the real interface |
| snapshots of hand-written values | proves serialization of the fixture more than governed logic |
| example-only tests for property claims | misses the universal claim stated by the spec |
| async tests holding locks across await | creates deadlocks and hides the real concurrency design |
| browser tooling for non-browser code | adds cost without stronger evidence |
| compile-time claims tested at runtime | misses the actual contract |
| source text read from tests | proves implementation text rather than behavior |
| missing harness cleanup | leaves shared state that changes later test outcomes |
| test-file-local constants for source-owned values | production module should export the value; refactor it |
Do not require spx validation literal for Rust tests. The literal validator is TypeScript-only. Enforce source-owned values through review and Rust test standards instead.
levels/level-1.md- pure computation, tempdir, trait seams, and property testslevels/level-2.md- CLI binaries, async adapters, local services, and containerized collaboratorslevels/level-3.md- remote systems, browser flows, credentials, isolation, and cleanup
Source: outcomeeng/plugins — distributed by TomeVault.