Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Every way to test Rust code

Unit tests, integration tests, doc tests, table tests, property tests, test doubles, fuzzing, Miri, and tests for async code and web services. What each kind catches.

Ask what you're trying to catch.

  • A function gives the wrong answer for an input you can name → a unit test in a #[cfg(test)] mod tests beside the code. Several inputs with the same shape → a table of cases in one test.
  • The public API doesn't work the way a user would call it → an integration test in tests/, which sees only what's pub.
  • The example in your documentation is wrong → a doc test. It runs with cargo test and keeps the docs honest.
  • Some input you didn't think of breaks it → a property test (state a rule that must hold for every input and let the tool search), or a fuzzer for a parser that takes untrusted bytes.
  • The code talks to something slow or unpredictable (the clock, the network, a payment service) → put that dependency behind a trait and pass a test double.
  • Unsafe code might have undefined behaviour → run the tests under Miri.
  • The code is async, or a whole HTTP service → #[tokio::test], paused time, and requests sent to the router without a real port.

The three built-in kinds

#[test], assert_eq!, and #[should_panic].

Each file in tests/ is its own crate that uses yours from outside.

Formal. Each .rs file directly inside a package's tests/ folder is compiled by cargo test as a separate crate that depends on the package's library crate. It can therefore use only items reachable through the library's public API, exactly as another crate could. Binary crates (src/main.rs) cannot be imported by integration tests at all, which is one reason to put logic in src/lib.rs.

In plain English. A test in the tests/ folder is an outsider. It uses your library the same way a stranger would, through the front door, so it checks what users will really experience.

Example. If src/lib.rs has a private fn normalize, a unit test inside src/lib.rs can call it, but tests/api.rs gets E0603.

Why this rule exists. Testing through the public API catches mistakes in what users actually see, and it lets the internals change without rewriting the tests.

Why an integration test can't reach a private helper, and why that's the point.

Code in a doc comment is compiled and run.

More inputs than you'd write by hand

A table of cases, then a rule checked against hundreds of generated inputs.

cargo fuzz for untrusted input, and cargo nextest for large suites.

A test that checks a rule holds for hundreds of random inputs instead of a few chosen examples.

Example

"Parsing what format_cents printed gives back the same number" checked for thousands of random amounts, not just 0, 1 and 999.

Isolating the code under test

Shared setup, temporary directories, and tests that don't depend on each other's order.

The smallest trait for the dependency, and a hand-written double.

Logic in lib.rs so tests can call it.

Special cases

#[tokio::test], timeouts so a hang fails, and paused time for retries.

Send requests straight to the router, with no network and no fixed port.

Miri runs your tests and reports undefined behaviour that normal runs miss.

When a test fails

Run just that one, show its output, and narrow it down.

Section

Tests in CI

Every test, on every target OS, on every change.