Testing in Rust

Course 2 · Ch 7
Testing in Rust
Verifying everything Course 2 built actually works — the fitting close to this course

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

#[test] fn it_adds_two_numbers() { assert_eq!(add(2, 3), 5); }

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:

pub fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn it_adds_two_numbers() { assert_eq!(add(2, 3), 5); } }

#[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:

// tests/integration_test.rs use my_crate::add; #[test] fn public_add_works() { assert_eq!(add(2, 2), 4); }
Unit TestsIntegration Tests
LocationSame file, #[cfg(test)] modtests/ directory
Compiled asPart of the same crateA separate crate per file
Can accessPrivate and public codePublic API only

Useful cargo test Options

  • cargo test add — runs only tests whose name contains "add"
  • cargo test -- --nocapture — shows println! 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 with cargo test -- --ignored
#[test] #[should_panic] fn rejects_a_negative_age() { Age::new(-1); // this test PASSES because new() is expected to panic here }

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.

GoRust
Test discoveryNaming convention (TestXxx in _test.go)#[test] attribute
Reporting failureExplicit t.Error()/t.Fatal() callsA panic — no separate reporting call
Unit vs. integration separationFile naming/build tags conventionSame-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.

A Fitting Close to Course 2
Testing is the right note to end this course on — it's the tool that lets you actually trust that lifetimes, traits, generics, smart pointers, and concurrent code all behave as intended, especially once Chapter 6's workspaces come into play with multiple crates each needing their own test coverage.
Forgetting #[cfg(test)] Bloats Every Regular Build
Without #[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

Challenge 1

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 solution
Challenge 2

Write 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 solution
Challenge 3

Explain 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 solution

Chapter 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.