Thuta Learning
Rust
AdvancedProgrammingbeginner

Testing and Error-Handling Strategy

What you'll walk away with

  • Explain the core ideas behind Testing and Error-Handling Strategy
  • Run the sample Rust code and verify its output
  • Apply the technique correctly to the Tutorial Platform and production scenarios

Build the mental model

Placing the `#[test]` attribute on a function tells `cargo test` to run it automatically, and "passing" means the function ran without panicking — `assert_eq!`/`assert!` macros panic on a failed condition and produce a failure message for you. Unit tests are typically written inside `src/` (via `#[cfg(test)] mod tests { ... }`), able to directly access even private functions to confirm small pieces of logic are correct, while integration tests live separately under `tests/`, calling only the public API to simulate real usage patterns. There's a practical rule of thumb for deciding between Lesson 11's `Result<T, E>` (recoverable) and `panic!` (unrecoverable): if input comes from an external source (user input, a file, the network) and might legitimately be invalid, use `Result` and hand the decision to the caller; but if code is violating an internal invariant ("this situation should never happen if the code is correct"), `panic!` is appropriate — stopping immediately is safer than risking data corruption by continuing. Library crates should minimize `panic!` and prefer `Result`, letting the library's caller decide how to handle errors — but in an application binary (`main.rs`), `.unwrap()`/`.expect()` can be a reasonable, pragmatic choice in situations where continuing would genuinely be dangerous.

Connect it to a real scenario

Write a unit test for the content analyzer's `count_words`: `#[test] fn counts_words_correctly() { assert_eq!(count_words("a b c"), 3); }` — running `cargo test` after every refactor immediately catches regressions. Write an integration test for `read_lesson` in `tests/read_lesson_test.rs`, creating an actual test fixture file on disk and calling the public API end-to-end. Calling the file path parameter with an empty string ("") represents plausibly invalid caller input, so return `Result::Err` — but if the running `total` variable's type overflows while accumulating word counts (an internal invariant that should theoretically never be violated), reach for `panic!`/`debug_assert!` instead.

Try the working example

rust
fn count_words(text: &str) -> u32 {
    text.split_whitespace().count() as u32
}

fn read_lesson_body(path: &str) -> Result<String, String> {
    if path.is_empty() {
        return Err(String::from("path must not be empty"));
    }
    Ok(format!("contents of {path}"))
}

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

    #[test]
    fn counts_words_correctly() {
        assert_eq!(count_words("rust ownership basics"), 3);
    }

    #[test]
    fn rejects_empty_path() {
        assert!(read_lesson_body("").is_err());
    }
}
You should see
Running `cargo test` reports "test result: ok. 2 passed" in the terminal.

5-minute try-it

Write three `#[test]` functions for `fn classify(word_count: u32) -> &'static str` (from Lesson 5) — check the expected output for boundary values (499, 500, 1500) with `assert_eq!`.

One important caution

Assuming external input (a user-provided file path, a network response) never needs validation and using `panic!`/`.unwrap()` everywhere — a simple user typo can crash the whole program; handle it gracefully with `Result` instead.

Over-testing private implementation details (every internal helper individually) with unit tests — when tests break on every implementation change, they're testing implementation instead of behavior, making refactoring painful.

The Rust Programming Language — Writing Automated TestsRust

Easy traps

  • Assuming external input (a user-provided file path, a network response) never needs validation and using `panic!`/`.unwrap()` everywhere — a simple user typo can crash the whole program; handle it gracefully with `Result` instead.
  • Over-testing private implementation details (every internal helper individually) with unit tests — when tests break on every implementation change, they're testing implementation instead of behavior, making refactoring painful.
  • Validate sample code in a local or test environment before applying it to a production system.

Exercise

Write three `#[test]` functions for `fn classify(word_count: u32) -> &'static str` (from Lesson 5) — check the expected output for boundary values (499, 500, 1500) with `assert_eq!`.

You'll know it worked when: Running `cargo test` reports "test result: ok. 2 passed" in the terminal.

Testing and Error-Handling Strategy | Thuta Learning