Testing in Rust
Course 2 has built a real project's worth of Rust knowledge — lifetimes, traits, generics, smart pointers, concurrency, modules. This final chapter closes with the tool that lets you trust all of it actually behaves as intended: Rust's built-in testing framework.
The #[test] Attribute
A function annotated #[test] becomes a test case, run with cargo test. A panic inside a test function is a failed test — no separate failure mechanism exists. This is a genuinely elegant reuse of Course 1 Chapter 7's panic! material: testing failure is just triggering a panic, nothing new to learn.
Organizing Unit Tests
The conventional Rust pattern: unit tests live in the same file as the code they test, inside a #[cfg(test)] submodule — a real departure from many languages' separate-test-directory convention:
#[cfg(test)] means this module compiles only when running tests, never in a normal build. use super::* brings the parent module's items — including private ones — into the test module's scope, so unit tests can exercise private implementation details directly, not just the public API.
Integration Tests
A separate top-level tests/ directory, sibling to src/, holds integration tests. Each file inside it compiles as its own separate crate, testing only the library's public API — exactly as an external consumer would use it:
| Unit Tests | Integration Tests | |
|---|---|---|
| Location | Same file, #[cfg(test)] mod | tests/ directory |
| Compiled as | Part of the same crate | A separate crate per file |
| Can access | Private and public code | Public API only |
Useful cargo test Options
cargo test add— runs only tests whose name contains "add"cargo test -- --nocapture— showsprintln!output even for passing tests (captured/hidden by default)#[should_panic]— a test that passes only if the function panics, useful for testing error conditions#[ignore]— skips a slow test by default; run explicitly withcargo test -- --ignored
Contrast With Go's Testing Package (go3-4)
Go's built-in testing uses a naming convention instead of an attribute — a function named TestXxx(t *testing.T) in a _test.go file is automatically discovered, no annotation required. Go failures are reported explicitly via t.Error()/t.Fatal() calls rather than Rust's "a panic is a failure" approach. Both languages ship testing built-in with no external framework needed — the actual mechanics of what counts as a test, and what counts as a failure, genuinely differ.
| Go | Rust | |
|---|---|---|
| Test discovery | Naming convention (TestXxx in _test.go) | #[test] attribute |
| Reporting failure | Explicit t.Error()/t.Fatal() calls | A panic — no separate reporting call |
| Unit vs. integration separation | File naming/build tags convention | Same-file #[cfg(test)] vs. a separate tests/ directory |
#[test]
Marks a function as a test case; a panic inside it means the test failed.
#[cfg(test)] mod tests
Same-file unit tests, compiled only when testing, with access to private code.
tests/ Directory
Integration tests, each file its own crate, exercising only the public API.
#[should_panic]
A test that passes specifically because the tested code panics — for verifying error conditions.
#[cfg(test)] on the test module, its code — and anything it pulls in via use super::* — gets compiled into every normal build, not just test runs. This inflates binary size and can unintentionally require test-only dependencies in production builds. Not catastrophic, but a real, common oversight worth double-checking.
Coding Challenges
Write a function subtract(a: i32, b: i32) -> i32 alongside a #[cfg(test)] mod tests block containing two #[test] functions: one verifying a normal case, one verifying subtracting a number from itself yields zero.
📄 View solutionWrite a function divide(a: f64, b: f64) -> f64 that panics if b is 0.0, and a #[test] #[should_panic] test verifying that dividing by zero actually panics.
📄 View solutionExplain why a unit test inside a #[cfg(test)] mod tests block can call a private function directly, while a test inside the tests/ directory cannot — referencing what "use super::*" actually brings into scope and what a tests/ file compiles as instead.
📄 View solutionChapter 7 Quick Reference
- #[test] — marks a function as a test case, run by cargo test; a panic = a failure
- assert!/assert_eq!/assert_ne! — the standard assertion macros used inside tests
- #[cfg(test)] mod tests { use super::*; } — same-file unit tests, compiled only when testing, private+public access
- tests/ directory — integration tests, each file its own crate, public API only
- #[should_panic] — a test that passes only if the code panics; #[ignore] — skip unless run explicitly
- Go discovers tests by naming convention (TestXxx) and reports failure via explicit t.Error(); Rust uses an attribute and treats any panic as failure
- Forgetting #[cfg(test)] compiles test code into every regular build, not just test runs
★ Rust Intermediate Complete — 7 / 7 chapters
From lifetimes through traits, generics, smart pointers, concurrency, modules, and finally testing — Course 2 has built the tools needed for real, growing Rust projects. Next up: Rust Advanced, covering advanced traits, unsafe Rust, async Rust, macros, and performance — closing with a capstone CLI tool.