Skip to content
DocsRust LearningexpertTesting
Chapter 17 of 19·expert·12 min read

Testing

Kiểm Thử

Unit, integration, and doc tests

Hover or tap any paragraph to see Vietnamese translation

Testing in Rust

Rust has built-in testing support — no external framework needed. Simply add the #[test] attribute to a function and cargo test automatically finds and runs it. This encourages writing tests alongside code rather than as an afterthought.

Unit Tests with #[test]

Unit tests test a small piece of code in isolation. In Rust, you typically place unit tests in the same file as the code they test, inside a tests module. This module is marked #[cfg(test)] so it only compiles when running tests.

unit_tests.rs
1pub fn add(a: i32, b: i32) -> i32 {2    a + b3}45pub fn divide(a: f64, b: f64) -> Option<f64> {6    if b == 0.0 {7        None8    } else {9        Some(a / b)10    }11}12

assert!, assert_eq!, assert_ne!

Rust provides several assertion macros. assert!(condition) checks a boolean condition. assert_eq!(left, right) checks two values are equal and prints both on failure. assert_ne!(left, right) checks two values are different. All support custom failure messages.

assertion_macros.rs
1#[cfg(test)]2mod tests {3    #[test]4    fn demonstrate_assertions() {5        // assert!: panics if false6        assert!(1 + 1 == 2);7        assert!(true, "this should be true");89        // assert_eq!: panics if left != right, prints both values10        assert_eq!(4, 2 + 2);11        assert_eq!(12            vec![1, 2, 3],

Testing for Panics with #[should_panic]

Sometimes you want to verify your code panics in certain situations. The #[should_panic] attribute marks a test expected to panic. You can add expected = "..." to verify a specific panic message substring.

should_panic.rs
1pub fn new_connection(port: u16) -> String {2    if port == 0 {3        panic!("port cannot be zero");4    }5    format!("connected on port {}", port)6}78pub fn get_first(v: &[i32]) -> i32 {9    if v.is_empty() {10        panic!("slice is empty");11    }12    v[0]
Tip
should_panic(expected = ...) matches a substring, not the full message. This makes tests more stable when panic messages change slightly.

Result<T, E> in Tests

Instead of using assert!, you can write tests that return Result<(), E>. If a test returns Err, the test fails with the error message. This lets you use the ? operator in tests for concise error propagation.

result_in_tests.rs
1use std::num::ParseIntError;23fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {4    let n: i32 = s.parse()?;5    Ok(n * 2)6}78#[cfg(test)]9mod tests {10    use super::*;1112    // Return Result from tests to use ? operator

Controlling Test Execution

cargo test provides many options to control how tests run. You can filter tests by name, run a single test, skip marked tests, and control parallel thread count.

test_execution_flags.rs
1// cargo test flags:23// Run all tests4// cargo test56// Run tests whose names contain "parse"7// cargo test parse89// Run a single test by exact name10// cargo test tests::test_parse_valid1112// Show stdout even for passing tests

Test Organization (tests module)

The Rust convention is to place unit tests in the same file as the code inside a tests module. This module has access to private functions because it is in the same crate. #[cfg(test)] ensures this code is not compiled in final release builds.

test_organization.rs
1// src/lib.rs23pub fn public_add(a: i32, b: i32) -> i32 {4    private_add(a, b)5}67// Private function8fn private_add(a: i32, b: i32) -> i32 {9    a + b10}1112#[cfg(test)]

Integration Tests (tests/ Directory)

Integration tests live in the tests/ directory at the root of your crate (same level as src/). Each file in tests/ is a separate crate and can only access your crate's public API. This is how you test that the public parts of your library work correctly as used from outside.

integration_tests.rs
1// File structure:2// my_crate/3//   Cargo.toml4//   src/5//     lib.rs6//   tests/7//     integration_test.rs8//     another_test.rs9//     common/10//       mod.rs   <- shared helpers (not run as a test file)1112// tests/integration_test.rs
Info
Integration tests can only call public functions. If you need to test internal logic, that is a unit test and belongs in src/ with #[cfg(test)].

Doc Tests

Rust can run code examples in documentation comments as tests. This ensures your documentation is always accurate and up to date. Every ``` code block in /// comments automatically becomes a doc test.

doc_tests.rs
1/// Adds two integers and returns the sum.2///3/// # Examples4///5/// ```6/// let result = my_crate::add(2, 3);7/// assert_eq!(result, 5);8/// ```9///10/// Negative numbers work too:11///12/// ```

Test Fixtures and Setup

Rust has no built-in setup/teardown like JUnit's @BeforeEach, but several patterns exist for reusing setup logic. You can use helper functions, once_cell, or crates like rstest to achieve similar results.

test_fixtures.rs
1use std::collections::HashMap;23// Pattern 1: Setup function called in each test4fn create_test_data() -> HashMap<String, i32> {5    let mut map = HashMap::new();6    map.insert("alice".to_string(), 95);7    map.insert("bob".to_string(), 87);8    map.insert("charlie".to_string(), 92);9    map10}1112#[cfg(test)]

Mocking Strategies

Rust has no built-in mocking framework but several approaches exist. The most idiomatic Rust approach is using traits to abstract dependencies, then providing fake implementations for tests. The mockall crate provides more sophisticated automatic mock generation.

mocking_strategies.rs
1// Strategy 1: Trait-based mocking (most idiomatic)2trait EmailService {3    fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), String>;4}56struct UserService<E: EmailService> {7    email: E,8}910impl<E: EmailService> UserService<E> {11    fn register(&self, email: &str) -> Result<(), String> {12        // ... create user ...

Code Coverage Basics

Code coverage measures what percentage of your code is executed during tests. Rust supports coverage measurement via cargo-llvm-cov or grcov. This is a useful tool for finding untested parts of your codebase.

code_coverage.rs
1// Install cargo-llvm-cov:2// cargo install cargo-llvm-cov34// Run tests with coverage report:5// cargo llvm-cov67// Generate HTML report:8// cargo llvm-cov --html9// open target/llvm-cov/html/index.html1011// Generate lcov format (for CI tools):12// cargo llvm-cov --lcov --output-path lcov.info
Tip
Aiming for 100% coverage is not always practical or valuable. Focus on testing important business logic and edge cases rather than chasing the highest coverage number.

Key Takeaways

Điểm Chính

  • #[test] marks functions as test cases run by cargo test#[test] đánh dấu hàm là test case chạy bởi cargo test
  • assert!, assert_eq!, assert_ne! are the primary test assertionsassert!, assert_eq!, assert_ne! là các assertion test chính
  • Integration tests live in the tests/ directory and test public APIsTest tích hợp nằm trong thư mục tests/ và kiểm thử API công khai
  • #[should_panic] tests that code panics as expected#[should_panic] kiểm tra code panic đúng như mong đợi

Practice

Test your understanding of this chapter

Quiz

When using #[should_panic(expected = '...')] how does Rust match the expected string against the actual panic message?

Khi dùng #[should_panic(expected = '...')] Rust so khớp chuỗi expected với thông điệp panic thực tế như thế nào?

Quiz

Which functions can an integration test file in the tests/ directory access?

File integration test trong thư mục tests/ có thể truy cập những hàm nào?

True or False

Code inside a #[cfg(test)] module is compiled into the final release binary.

Code bên trong module #[cfg(test)] được biên dịch vào binary phát hành cuối cùng.

True or False

A Rust test function can return Result<(), E> and use the ? operator to propagate errors; the test is marked as failed if the function returns Err.

Hàm test Rust có thể trả về Result<(), E> và dùng toán tử ? để truyền lỗi; test bị đánh dấu thất bại nếu hàm trả về Err.

Code Challenge

Fill in the OnceLock method for lazy shared test data initialization

Điền vào phương thức OnceLock để khởi tạo lười biếng dữ liệu test dùng chung

static DATA: OnceLock<Vec<i32>> = OnceLock::new();

fn get_data() -> &'static Vec<i32> {
    DATA.(|| (1..=100).collect())
}

Chapter Complete!

Great job! Keep the momentum going.

Your progress0 of 19 chapters read
← → to navigate chapters
Built: 4/8/2026, 12:01:11 PM