Rust has built-in support for writing unit tests using the #[test] attribute. You can write test functions inside a special tests module, use assertion macros like assert_eq!, and run your tests with cargo test. Testing is part of the standard development flow in Rust, not an afterthought.
When I started writing Rust seriously, I was impressed by how straightforward and integrated the testing tools were. I did not need external libraries or setup. I just wrote a test function, added #[test] on top, and ran cargo test. It felt like testing was built into the language at the core, and it is.
In this post, I will walk you through the basics of unit testing in Rust, from writing your first test to organizing test modules and using useful macros. You will also see how tests help you write more confident and maintainable code.
How to Write a Basic Test
You write unit tests using regular Rust functions with a #[test] attribute above them.
Example:
#[cfg(test)]
mod tests {
#[test]
fn test_addition() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}
You must wrap tests in a #[cfg(test)] mod tests block so they are only compiled when running tests.
To run the tests:
cargo test
Using assert Macros
Rust provides several built-in macros to write checks:
- assert!(condition) – passes if true
- assert_eq!(a, b) – passes if a == b
- assert_ne!(a, b) – passes if a != b
Example:
#[test]
fn test_length() {
let s = "rustacean";
assert!(s.len() > 5);
assert_eq!(s.len(), 9);
}
If a test fails, the test runner shows which assertion failed and what the values were.
Testing Functions from Your Code
Assume you have a function in your library:
pub fn square(x: i32) -> i32 {
x * x
}
You can write tests like this:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_square() {
assert_eq!(square(3), 9);
assert_eq!(square(-4), 16);
}
}
Using super:: gives you access to functions from the parent module.
Testing for Expected Failures
You can write a test that should panic:
#[test]
#[should_panic]
fn test_panic_case() {
panic!("This test should panic");
}
You can even test for specific panic messages:
#[test]
#[should_panic(expected = "divide by zero")]
fn test_divide_zero() {
let _ = 1 / 0;
}
This ensures that your code panics in the right situations.
Test Output and Filtering
Run all tests:
cargo test
Run only tests matching a name:
cargo test square
Show all output (even passed tests):
cargo test -- --show-output
This is useful when debugging tests with println! inside.
Test Organization
Put tests in the same file as the code for small modules. For bigger projects:
- Use a separate tests/ folder for integration tests (covered in the next post).
- Group tests by functionality.
- Use helper functions in the test module to avoid repetition.
Ignoring Tests
You can temporarily disable a test using #[ignore]:
#[test]
#[ignore]
fn test_slow_case() {
// slow or flaky test
}
Run only ignored tests:
cargo test -- --ignored
This is useful for skipping time-consuming tests in normal test runs.
Testing Expected Results and Errors
Test a function that returns Result:
fn double_even(x: i32) -> Result<i32, &'static str> {
if x % 2 == 0 {
Ok(x * 2)
} else {
Err("not even")
}
}
#[test]
fn test_even_ok() {
assert_eq!(double_even(4), Ok(8));
}
#[test]
fn test_odd_err() {
assert_eq!(double_even(3), Err("not even"));
}
This pattern is especially common when your app uses custom error types (see our post on that).
Summary
Rust makes unit testing a first-class part of development. You can define tests using #[test], organize them in test modules, and run them with cargo test. With assert macros, test filtering, and expected failures, you can write expressive tests that boost your code’s reliability. The more you write tests, the more you understand and trust your code.
Rust Intermediate Concepts
- Generic Types in Functions and Structs
- Implementing Traits for Custom Behavior in Rust
- Trait Bounds and Constraints in Rust
- Lifetimes in Rust
- Using Closures (Anonymous Functions) in Rust
- The Iterator Trait and .next() in Rust
- Higher Order Iterator Methods: map, filter, and fold in Rust
- Using impl Trait for Simplicity in Rust
- Advanced Collections: HashSet and BTreeMap in Rust
- Custom Error Types and the Error Trait in Rust
- Option and Result Combinators in Rust
- Writing Unit Tests in Rust
- Integration Testing in Rust
- Logging in Rust with the log Crate
- Cargo Tips and Tricks for Rust Projects
- CLI Argument Parsing with Clap in Rust
- File I/O and File System Operations in Rust
- Running External Commands in Rust
- Make HTTP Requests in Rust with Reqwest
- JSON Serialization and Deserialization in Rust with Serde
- Building a Weather CLI in Rust
- Date and Time Handling in Rust with Chrono
- Using Regular Expressions in Rust with the regex Crate
- Memory Management in Rust
- Understanding Borrow Checker Errors in Rust
- Interacting with Databases in Rust
- Building a Todo List CLI in Rust with SQLite
- Concurrency in Rust