Files
skills/skills/tdd/references/rust.md
T
mathias 8da7c9c45f docs(tdd): make the spine language-neutral, add four language references
The discipline in tdd was already language-agnostic; the checklist under it was
not. A Python user inherited a mandatory `go test -race` box they can never tick
and never got told that `mypy --strict` belongs in the red gate.

Per #18's own comment and writing-skills' Multi-Language Dilution warning, the
main SKILL.md keeps ONE canonical inline example (Go, the default stack) and the
other four languages live in reference files: python, typescript, rust, java.

The RED run drove every change. A subagent given a Python task with the Go-only
skill reported, in its own words:

- "Everything from L169 to L244 is Go-only, and the verification checklist at
  L326 silently inherits that Go-ness — which is why a Python user ends up with
  a mandatory box (-race) they can never tick and a required gate (mypy
  --strict) that isn't listed at all."
- "For a brand-new function, the first RED is always a collection-time
  ImportError... The only clean resolutions are a stub that raises
  NotImplementedError (production code before a red test?) or accepting
  ImportError as a legitimate red. The skill picks neither, and this is the
  single biggest gap." It now picks: that red is legitimate, and writing a stub
  to satisfy the runner is the Iron Law with extra steps.
- The Go table-driven exemplar branches on a `wantErr` column inside the loop.
  Translated literally that is a conditional assertion folding two behaviours
  into one test — which this skill's own quality table forbids. The agent caught
  itself mid-translation; both SKILL.md and python.md now say not to port it.

Also: the checklist names the static-analysis gate per language instead of
assuming `go vet`, and says to strike the concurrency box deliberately where no
equivalent exists — a box that cannot be ticked teaches people to skip
checklists.

Refs #18
Bump-Type: minor
2026-08-29 14:10:10 +02:00

2.8 KiB

TDD in Rust

The Iron Law and Red-Green-Refactor in SKILL.md apply unchanged. This file replaces the Go-Specific TDD Notes for Rust.

The first red is a compile error, and that is correct

error[E0425]: cannot find function `split_vat` in this scope

Legitimate — the compiler is the test runner's first phase. Read it, confirm it names the item you are about to write, and go to GREEN. Do not write a todo!() stub first; that is production code before a failing test. (todo!() is fine during GREEN as a branch you have not reached yet.)

Where tests live

  • Unit tests in the same file, in mod tests behind #[cfg(test)]. They can reach private items — the white-box option Go spells package foo.
  • Integration tests in tests/, which link the crate as an external consumer and see only the public API. This is the black-box default SKILL.md prefers; use it unless the behaviour under test is genuinely private.
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn tie_rounds_down_to_even() {
        assert_eq!(split_vat(14, dec!(0.12)).unwrap(), (12, 2));
    }
}

Table-driven: an array of cases

#[test]
fn rounds_half_to_even() {
    let cases = [
        ("tie rounds down", 14u64, dec!(0.12), (12, 2)),
        ("tie rounds up", 42, dec!(0.12), (38, 4)),
        ("exact division", 12500, dec!(0.25), (10000, 2500)),
    ];
    for (name, amount, rate, want) in cases {
        assert_eq!(split_vat(amount, rate).unwrap(), want, "case: {name}");
    }
}

The "case: {name}" message is the t.Run analogue — without it a failure names a line number and leaves you counting rows. If you want each case to be its own reported test, use rstest's #[case]; that is a dependency, so justify it before adding it.

Error cases

Result makes the Go wantErr column unnecessary — assert on the variant:

#[test]
fn rejects_negative_amount() {
    let err = split_vat_signed(-1, dec!(0.25)).unwrap_err();
    assert!(matches!(err, VatError::NegativeAmount));
}

#[should_panic(expected = "...")] is for genuine panics only — invariant violations, not expected error paths. A function whose contract includes "rejects negative input" should return Err, not panic, and a test that accepts a panic hides that design choice.

Gates

cargo test                    # the failing test
cargo clippy -- -D warnings   # static gate, same phase as RED
cargo fmt --check

cargo test compiles in debug, so integer overflow panics rather than wrapping — a release build will not. Where overflow matters, test the checked path explicitly (checked_mul), or you are testing debug-only behaviour.

For unsafe code, cargo +nightly miri test is the nearest thing to Go's -race. Safe-only crates have no equivalent and should strike that checklist box deliberately.