# 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. ```rust #[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 ```rust #[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: ```rust #[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 ```bash 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.