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.
Rust có hỗ trợ kiểm thử tích hợp sẵn — bạn không cần cài đặt framework bên ngoài. Chỉ cần thêm thuộc tính #[test] vào một hàm, và cargo test sẽ tự động tìm và chạy nó. Điều này khuyến khích viết test cùng với code thay vì để sau.
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 test kiểm tra một phần nhỏ của code một cách riêng lẻ. Trong Rust, bạn thường đặt unit test trong cùng file với code mà chúng kiểm tra, bên trong một module tests. Module này được đánh dấu #[cfg(test)] để chỉ biên dịch khi chạy test.
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}12Rust 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.
Rust cung cấp nhiều macro assertion. assert!(condition) kiểm tra một điều kiện boolean. assert_eq!(left, right) kiểm tra hai giá trị bằng nhau và in cả hai nếu thất bại. assert_ne!(left, right) kiểm tra hai giá trị khác nhau. Tất cả đều hỗ trợ thông điệp lỗi tùy chỉnh.
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],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.
Đôi khi bạn muốn kiểm tra rằng code của mình sẽ panic trong những tình huống nhất định. Thuộc tính #[should_panic] đánh dấu một test dự kiến sẽ panic. Bạn có thể thêm expected = '...' để kiểm tra thông điệp panic cụ thể.
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]should_panic(expected = ...) so khớp chuỗi con (substring), không phải toàn bộ thông điệp. Điều này giúp test ổn định hơn khi thông điệp panic thay đổi một chút.
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.
Thay vì dùng assert!, bạn có thể viết test trả về Result<(), E>. Nếu test trả về Err, test thất bại với thông điệp lỗi. Điều này cho phép bạn dùng toán tử ? trong test để truyền lỗi ngắn gọn hơn.
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 ? operatorcargo 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.
cargo test cung cấp nhiều tùy chọn để kiểm soát cách test được chạy. Bạn có thể lọc test theo tên, chạy test đơn lẻ, bỏ qua các test được đánh dấu, và kiểm soát số luồng song song.
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 testsThe 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.
Quy ước Rust là đặt unit test trong cùng file với code bên trong một module tests. Module này có quyền truy cập vào các hàm private vì nó nằm trong cùng crate. #[cfg(test)] đảm bảo code này không được biên dịch trong bản phát hành cuối.
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 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 test nằm trong thư mục tests/ ở gốc của crate (cùng cấp với src/). Mỗi file trong tests/ là một crate riêng biệt và chỉ có thể truy cập API public của crate của bạn. Đây là cách kiểm tra rằng các phần công khai của thư viện hoạt động đúng khi được dùng từ bên ngoài.
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.rsIntegration test chỉ có thể gọi các hàm public. Nếu bạn cần kiểm tra logic nội bộ, đó là unit test và nên ở trong src/ với #[cfg(test)].
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.
Rust có thể chạy các ví dụ code trong comment tài liệu (doc comments) như test. Điều này đảm bảo tài liệu của bạn luôn chính xác và cập nhật. Mọi khối code ``` trong /// comments đều tự động trở thành doc test.
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/// ```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.
Rust không có tính năng setup/teardown tích hợp như JUnit's @BeforeEach, nhưng có nhiều mẫu để tái sử dụng logic setup. Bạn có thể dùng hàm helper, lazy_static, hoặc các crate như rstest để đạt được kết quả tương tự.
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)]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.
Rust không có framework mock tích hợp sẵn nhưng có nhiều cách để mock. Cách Rust-ish nhất là dùng trait để trừu tượng hóa các dependency, sau đó tạo các implementation giả cho test. Crate mockall cung cấp khả năng tự động sinh mock phức tạp hơn.
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 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.
Độ phủ code (code coverage) đo tỷ lệ phần trăm code của bạn được thực thi trong quá trình test. Rust hỗ trợ đo độ phủ thông qua cargo-llvm-cov hoặc grcov. Đây là công cụ hữu ích để tìm ra những phần code chưa được kiểm tra.
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.infoMục tiêu đạt độ phủ 100% không phải lúc nào cũng thực tế hoặc có giá trị. Tập trung vào kiểm thử logic kinh doanh quan trọng và các trường hợp biên hơn là đuổi theo con số phủ code cao nhất.
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
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?
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?
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.
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.
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()) }