Testing and Doctests
Rule
Test behavior at the level where it is observable and risky: focused unit tests for local logic, integration tests for public workflows, and doctests only by explicit opt-in.
Why
Test placement should protect behavior without creating file quotas or coupling coverage to implementation structure.
Do
- Test invariants, outputs, errors, and observable state changes.
- Put focused logic, parsers, validation, and transformations in nearby unit tests.
- Put public APIs, CLI behavior, I/O boundaries, and workflows in integration tests.
- Choose placement from behavior and risk, not source-file count.
- Name tests as behaviors such as
rejects_zero_limit. - Return
Resultfrom tests when setup naturally uses?. - Keep fixtures and helpers small and domain-named.
- Use real values and temporary resources where practical; fake only slow, external, or nondeterministic boundaries.
- Put regression tests where the bug was observable.
Avoid
- Do not require a test module in every nontrivial source file.
- Do not mirror private implementation steps or every helper.
- Do not use rustdoc examples as default coverage.
- Do not add sleeps or timing dependence.
- Do not make helpers public solely for integration tests.
- Do not add broad test-only controls to production APIs.
- Do not settle for “does not panic” when behavior can be asserted.
Example
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_zero_limit() {
let error = parse_limit("0").expect_err("zero should be rejected");
assert_eq!(error, LimitError::Zero);
}
}
}
An end-to-end create-user workflow belongs under tests/ because callers observe it through the public application surface.
Exceptions
- Add doctests only when the project opts into maintaining public examples.
- Use module-private tests for dense parser, validator, state-machine, or algorithm edge cases.
- Gate deliberate reusable test support behind a
test-utilfeature.