Testing in Rust: unit, integration, e2e from A to Z

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why testing in Rust is not just trendy, but necessary

\n\n

Rust is a language renowned for its memory safety and high performance. However, even the strictest compiler does not guarantee the correctness of business logic. This is where testing comes into play. In Rust, the type and ownership system already eliminates a whole class of errors at the compilation stage, but unit, integration, and end-to-end (e2e) testing allow you to ensure that your code does exactly what you intended.

\n\n

In this article, we will break down three levels of testing in Rust: from isolated unit tests to full-fledged e2e scenarios. You will learn how to write effective tests, which tools to use, and how to organize the test infrastructure. The material will be useful both for beginners just starting to learn Rust and for experienced developers looking to systematize their knowledge.

\n\n

1. Unit testing: the foundation

\n\n

Unit tests are the fastest and most isolated level of verification. They test individual functions, methods, or small modules in isolation from external dependencies. In Rust, unit tests are typically placed in the same file as the code being tested, inside a tests module.

\n\n

1.1. Structure and attributes

\n\n

To create a unit test, use the #[test] attribute. A function marked with it takes no arguments and returns nothing (or returns a Result). If a panic occurs inside the function, the test is considered failed.

\n\n
// src/lib.rs\npub fn add(a: i32, b: i32) -> i32 {\n    a + b\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_add() {\n        assert_eq!(add(2, 2), 4);\n    }\n\n    #[test]\n    fn test_add_negative() {\n        assert_eq!(add(-1, -1), -2);\n    }\n}
\n\n

Note the #[cfg(test)] attribute. This ensures that the test module is compiled only when running tests (cargo test). It will not be included in the production build.

\n\n

1.2. Assertion macros

\n\n

Rust provides several macros for checking assertions:

\n\n
    \n
  • assert!(expr) — checks that the expression is true.
  • \n
  • assert_eq!(left, right) — checks for equality (requires PartialEq and Debug).
  • \n
  • assert_ne!(left, right) — checks for inequality.
  • \n
  • panic!("message") — forcibly triggers a panic (used for testing errors).
  • \n
\n\n

1.3. Testing panics with should_panic

\n\n

If your function should panic on invalid input, use the #[should_panic] attribute:

\n\n
pub fn divide(a: f64, b: f64) -> f64 {\n    if b == 0.0 {\n        panic!("Division by zero");\n    }\n    a / b\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    #[should_panic(expected = "Division by zero")]\n    fn test_divide_by_zero() {\n        divide(10.0, 0.0);\n    }\n}
\n\n

The expected parameter is optional, but it makes the test more precise: the panic must contain the specified string.

\n\n

2. Integration testing: checking module interaction

\n\n

Integration tests verify how different parts of your application work together. Unlike unit tests, they are located in a separate tests/ directory at the project root level (next to src/). Each file in this folder is compiled as a separate crate library.

\n\n

2.1. Creating an integration test

\n\n

Create a file tests/integration_test.rs:

\n\n
// tests/integration_test.rs\nuse my_crate::add; // assuming your crate is named my_crate\n\n#[test]\nfn test_add_integration() {\n    let result = add(5, 10);\n    assert_eq!(result, 15);\n}
\n\n

To run only integration tests

Blogs

Book Recommendations