Introduction: Why testing in Rust is not just trendy, but necessary
\n\nRust 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\nIn 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\n1. Unit testing: the foundation
\n\nUnit 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.
1.1. Structure and attributes
\n\nTo 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.
// 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\nNote 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.
1.2. Assertion macros
\n\nRust 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 (requiresPartialEqandDebug). \n assert_ne!(left, right)— checks for inequality. \n panic!("message")— forcibly triggers a panic (used for testing errors). \n
1.3. Testing panics with should_panic
\n\nIf your function should panic on invalid input, use the #[should_panic] attribute:
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\nThe expected parameter is optional, but it makes the test more precise: the panic must contain the specified string.
2. Integration testing: checking module interaction
\n\nIntegration 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.
2.1. Creating an integration test
\n\nCreate a file tests/integration_test.rs:
// 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\nTo run only integration tests