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
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 testsbehind#[cfg(test)]. They can reach private items — the white-box option Go spellspackage foo. - Integration tests in
tests/, which link the crate as an external consumer and see only the public API. This is the black-box defaultSKILL.mdprefers; 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.