Writing Unit Tests in Rust

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.