DaleSchool

Verifying Your Code: Testing

Intermediate15min

Learning Objectives

  • Write unit tests with #[test]
  • Use assert macros effectively
  • Run tests with cargo test and interpret the results
  • Understand the integration test directory structure

Working Code

Tests live right next to your library code for the fastest feedback loop. Place #[test] functions inside a #[cfg(test)] module — they won't be included in release builds.

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

pub fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        return Err("Cannot divide by zero".into());
    }
    Ok(a / b)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_works() {
        assert_eq!(add(2, 3), 5);
        assert_ne!(add(-1, 1), 0);
    }

    #[test]
    fn divide_returns_error() -> Result<(), String> {
        assert_eq!(divide(10, 2)?, 5);
        assert!(divide(5, 0).is_err());
        Ok(())
    }
}
  • assert_eq!, assert_ne!, and assert! show you exactly which values differed on failure.
  • When a test function returns Result<(), E>, you can use the ? operator for cleaner error handling.

Exploring cargo test

Run cargo test in the terminal and it automatically finds and runs all tests:

$ cargo test
   Compiling mylib v0.1.0 (/Users/dale/mylib)
    Finished test [unoptimized + debuginfo] target(s) in 1.15s
     Running unittests src/lib.rs (target/debug/deps/mylib-abc123)
test tests::add_works ... ok
test tests::divide_returns_error ... ok

failures:
    tests::add_works

---- tests::add_works stdout ----
thread 'tests::add_works' panicked at 'assertion failed: `(left == right)`
  left: `6`,
 right: `5`', src/lib.rs:8:9
  • ok / FAILED lines give you a quick overview.
  • Failed tests show a stack trace and a summary at the end for easy debugging.
  • Run a specific test with cargo test add_works.

Integration Test Structure

To verify behavior from outside the library, use the tests/ directory. Each file there is compiled as an independent binary, testing real usage scenarios:

my-project/
├─ src/
│  └─ lib.rs
├─ tests/
│  ├─ cli.rs
│  └─ api.rs
└─ Cargo.toml

Example tests/cli.rs:

use assert_cmd::Command;

#[test]
fn prints_help() {
    let mut cmd = Command::cargo_bin("todo")
        .expect("binary todo not found");
    cmd.arg("--help").assert().success();
}
  • Integration tests can only access public APIs. Test private internals with unit tests.
  • If you need shared helpers, put them in tests/common/mod.rs and import with mod common; from each test file.

Try It Yourself

  1. Create a fn greet(name: &str) -> String in src/lib.rs and write a test that verifies it returns "Hello, {name}".
  2. Intentionally write a failing test and observe how the cargo test output differs.
  3. Create a tests/ folder and write a short integration test to verify the library works as expected end-to-end.
  1. Use assert! instead of panic! — Rewrite the code below to use assert!/assert_eq! with descriptive messages instead of manual panic! calls.
  2. Result-returning tests — Assume a function fn parse_user_id(input: &str) -> Result<u32, String>. Write two tests: one for success and one for failure.
  3. Integration test — Assuming you have a CLI program, write a test in tests/cli.rs that runs the binary with Command::cargo_bin and checks stdout.

Q1. How does Rust identify test functions?

  • A) The function name must start with test_
  • B) The function must have the #[test] attribute
  • C) It must be inside a tests module
  • D) It must use the cfg(test) macro

Q2. What's the advantage of tests that return Result<(), E>?

  • A) Tests run faster
  • B) You can use the ? operator for concise error propagation
  • C) You can't use assert macros
  • D) cargo test hides error messages

Q3. Which statement about integration tests (tests/ folder) is correct?

  • A) They can access private functions
  • B) Each file is built as an independent binary
  • C) They only run with cargo test --lib
  • D) use statements can't be used in integration tests