✅ Unit Testing: Kiểm Tra Code
🎯 Mục Tiêu Bài Học
Sau khi hoàn thành bài học này, bạn sẽ:
- ✅ Viết unit tests với
#[test] - ✅ Sử dụng assert macros
- ✅ Tổ chức tests trong modules
- ✅ Chạy tests với
cargo test - ✅ Test private functions
- ✅ Test error cases
🤔 Testing Là Gì?
Ẩn Dụ Cuộc Sống: Kiểm Tra Chất Lượng
Testing giống như kiểm tra sản phẩm trước khi bán:
🏭 Nhà Máy Sản Xuất:
- Kiểm tra từng bộ phận (unit test)
- Đảm bảo hoạt động đúng
- Phát hiện lỗi sớm
- Tự tin khi ship
🦀 Testing Trong Rust:
- Unit tests - test từng function
- Tự động kiểm tra
- Ngăn bugs
- Documentation bằng examples
Ví Dụ Cơ Bản
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[test]
fn test_add() {
assert_eq!(add(2, 2), 4);
}
📝 #[test] Attribute
Định Nghĩa Test
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
Multiple Tests
fn multiply(a: i32, b: i32) -> i32 {
a * b
}
#[test]
fn test_multiply_positive() {
assert_eq!(multiply(2, 3), 6);
}
#[test]
fn test_multiply_negative() {
assert_eq!(multiply(-2, 3), -6);
}
#[test]
fn test_multiply_zero() {
assert_eq!(multiply(5, 0), 0);
}
✔️ Assert Macros
assert!
#[test]
fn test_is_positive() {
let x = 5;
assert!(x > 0);
assert!(x < 10);
}
assert_eq!
#[test]
fn test_equality() {
let result = 2 + 2;
assert_eq!(result, 4);
}
assert_ne!
#[test]
fn test_inequality() {
let x = 5;
let y = 10;
assert_ne!(x, y);
}
Custom Messages
#[test]
fn test_with_message() {
let x = 5;
assert!(x > 0, "x should be positive, got {}", x);
assert_eq!(x, 5, "x should equal 5");
}
🏃 Running Tests
cargo test
cargo test
Run Specific Test
cargo test test_add
Run Tests in Module
cargo test tests::
Show Output
cargo test -- --show-output
Run in Single Thread
cargo test -- --test-threads=1
📦 Test Organization
Tests Module
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 2), 4);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-2, 2), 0);
}
}
Multiple Test Modules
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn multiply(a: i32, b: i32) -> i32 {
a * b
}
#[cfg(test)]
mod addition_tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 2), 4);
}
}
#[cfg(test)]
mod multiplication_tests {
use super::*;
#[test]
fn test_multiply() {
assert_eq!(multiply(2, 3), 6);
}
}
🎯 Testing Functions
Simple Function
fn is_even(n: i32) -> bool {
n % 2 == 0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_even() {
assert!(is_even(2));
assert!(is_even(0));
assert!(is_even(-4));
}
#[test]
fn test_is_not_even() {
assert!(!is_even(1));
assert!(!is_even(3));
assert!(!is_even(-5));
}
}
Function with Structs
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_area() {
let rect = Rectangle {
width: 10,
height: 5,
};
assert_eq!(rect.area(), 50);
}
#[test]
fn test_can_hold() {
let rect1 = Rectangle {
width: 10,
height: 5,
};
let rect2 = Rectangle {
width: 5,
height: 3,
};
assert!(rect1.can_hold(&rect2));
}
#[test]
fn test_cannot_hold() {
let rect1 = Rectangle {
width: 5,
height: 3,
};
let rect2 = Rectangle {
width: 10,
height: 5,
};
assert!(!rect1.can_hold(&rect2));
}
}
❌ Testing Errors
should_panic
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Division by zero!");
}
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn test_divide_by_zero() {
divide(10, 0);
}
#[test]
#[should_panic(expected = "Division by zero")]
fn test_divide_by_zero_with_message() {
divide(10, 0);
}
}
Testing Result
fn safe_divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_safe_divide_ok() -> Result<(), String> {
let result = safe_divide(10, 2)?;
assert_eq!(result, 5);
Ok(())
}
#[test]
fn test_safe_divide_error() {
let result = safe_divide(10, 0);
assert!(result.is_err());
}
}
🎯 Ví Dụ Thực Tế
Ví Dụ 1: Calculator
struct Calculator;
impl Calculator {
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn subtract(a: i32, b: i32) -> i32 {
a - b
}
fn multiply(a: i32, b: i32) -> i32 {
a * b
}
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(Calculator::add(2, 3), 5);
assert_eq!(Calculator::add(-2, 3), 1);
}
#[test]
fn test_subtract() {
assert_eq!(Calculator::subtract(5, 3), 2);
assert_eq!(Calculator::subtract(3, 5), -2);
}
#[test]
fn test_multiply() {
assert_eq!(Calculator::multiply(2, 3), 6);
assert_eq!(Calculator::multiply(-2, 3), -6);
}
#[test]
fn test_divide() {
let result = Calculator::divide(10.0, 2.0).unwrap();
assert_eq!(result, 5.0);
}
#[test]
fn test_divide_by_zero() {
let result = Calculator::divide(10.0, 0.0);
assert!(result.is_err());
}
}
Ví Dụ 2: String Utilities
fn reverse_string(s: &str) -> String {
s.chars().rev().collect()
}
fn is_palindrome(s: &str) -> bool {
let s = s.to_lowercase().replace(" ", "");
s == reverse_string(&s)
}
fn count_words(s: &str) -> usize {
s.split_whitespace().count()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reverse() {
assert_eq!(reverse_string("hello"), "olleh");
assert_eq!(reverse_string("rust"), "tsur");
}
#[test]
fn test_palindrome() {
assert!(is_palindrome("racecar"));
assert!(is_palindrome("A man a plan a canal Panama"));
assert!(!is_palindrome("hello"));
}
#[test]
fn test_count_words() {
assert_eq!(count_words("hello world"), 2);
assert_eq!(count_words("one two three"), 3);
assert_eq!(count_words(""), 0);
}
}
Ví Dụ 3: Vector Operations
fn sum_vec(v: &[i32]) -> i32 {
v.iter().sum()
}
fn average_vec(v: &[i32]) -> f64 {
if v.is_empty() {
0.0
} else {
sum_vec(v) as f64 / v.len() as f64
}
}
fn max_vec(v: &[i32]) -> Option<i32> {
v.iter().max().copied()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum() {
assert_eq!(sum_vec(&[1, 2, 3, 4, 5]), 15);
assert_eq!(sum_vec(&[]), 0);
}
#[test]
fn test_average() {
assert_eq!(average_vec(&[1, 2, 3, 4, 5]), 3.0);
assert_eq!(average_vec(&[]), 0.0);
}
#[test]
fn test_max() {
assert_eq!(max_vec(&[1, 5, 3, 2]), Some(5));
assert_eq!(max_vec(&[]), None);
}
}
Ví Dụ 4: User Validation
struct User {
username: String,
email: String,
age: u32,
}
impl User {
fn new(username: &str, email: &str, age: u32) -> Result<Self, String> {
if username.is_empty() {
return Err(String::from("Username cannot be empty"));
}
if !email.contains('@') {
return Err(String::from("Invalid email"));
}
if age < 13 {
return Err(String::from("User must be at least 13 years old"));
}
Ok(User {
username: username.to_string(),
email: email.to_string(),
age,
})
}
fn is_adult(&self) -> bool {
self.age >= 18
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_user() {
let user = User::new("alice", "[email protected]", 20);
assert!(user.is_ok());
}
#[test]
fn test_empty_username() {
let user = User::new("", "[email protected]", 20);
assert!(user.is_err());
}
#[test]
fn test_invalid_email() {
let user = User::new("alice", "invalid-email", 20);
assert!(user.is_err());
}
#[test]
fn test_too_young() {
let user = User::new("alice", "[email protected]", 10);
assert!(user.is_err());
}
#[test]
fn test_is_adult() {
let user = User::new("alice", "[email protected]", 20).unwrap();
assert!(user.is_adult());
let user2 = User::new("bob", "[email protected]", 15).unwrap();
assert!(!user2.is_adult());
}
}
Ví Dụ 5: Shopping Cart
#[derive(Debug, PartialEq)]
struct Item {
name: String,
price: f64,
quantity: u32,
}
struct Cart {
items: Vec<Item>,
}
impl Cart {
fn new() -> Self {
Cart { items: vec![] }
}
fn add_item(&mut self, name: &str, price: f64, quantity: u32) {
self.items.push(Item {
name: name.to_string(),
price,
quantity,
});
}
fn total(&self) -> f64 {
self.items
.iter()
.map(|item| item.price * item.quantity as f64)
.sum()
}
fn item_count(&self) -> usize {
self.items.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_cart() {
let cart = Cart::new();
assert_eq!(cart.item_count(), 0);
assert_eq!(cart.total(), 0.0);
}
#[test]
fn test_add_item() {
let mut cart = Cart::new();
cart.add_item("Apple", 1.5, 3);
assert_eq!(cart.item_count(), 1);
}
#[test]
fn test_total() {
let mut cart = Cart::new();
cart.add_item("Apple", 1.5, 3); // 4.5
cart.add_item("Banana", 2.0, 2); // 4.0
assert_eq!(cart.total(), 8.5);
}
}
🔧 Testing Private Functions
Testing in Same Module
fn public_function(x: i32) -> i32 {
private_helper(x) * 2
}
fn private_helper(x: i32) -> i32 {
x + 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_public() {
assert_eq!(public_function(5), 12);
}
#[test]
fn test_private() {
// Can test private functions in same module
assert_eq!(private_helper(5), 6);
}
}
🎓 #[ignore] Tests
Ignoring Tests
#[test]
fn quick_test() {
assert_eq!(2 + 2, 4);
}
#[test]
#[ignore]
fn slow_test() {
// This test takes long time
std::thread::sleep(std::time::Duration::from_secs(5));
assert_eq!(2 + 2, 4);
}
Running Ignored Tests
cargo test -- --ignored
Running All Tests
cargo test -- --include-ignored
💻 Bài Tập Thực Hành
Bài 1: Test Simple Function
fn is_prime(n: u32) -> bool {
if n < 2 {
return false;
}
for i in 2..=(n as f64).sqrt() as u32 {
if n % i == 0 {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
// TODO: Viết tests cho is_prime
// Test: 2, 3, 5, 7 là prime
// Test: 0, 1, 4, 6 không phải prime
}
💡 Gợi ý
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prime_numbers() {
assert!(is_prime(2));
assert!(is_prime(3));
assert!(is_prime(5));
assert!(is_prime(7));
}
#[test]
fn test_non_prime_numbers() {
assert!(!is_prime(0));
assert!(!is_prime(1));
assert!(!is_prime(4));
assert!(!is_prime(6));
}
}
Bài 2: Test with Result
fn parse_positive(s: &str) -> Result<i32, String> {
let num: i32 = s.parse()
.map_err(|_| String::from("Invalid number"))?;
if num <= 0 {
return Err(String::from("Number must be positive"));
}
Ok(num)
}
#[cfg(test)]
mod tests {
use super::*;
// TODO: Test valid positive numbers
// TODO: Test invalid strings
// TODO: Test negative numbers
}
💡 Gợi ý
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_positive() {
assert_eq!(parse_positive("5").unwrap(), 5);
assert_eq!(parse_positive("100").unwrap(), 100);
}
#[test]
fn test_invalid_string() {
assert!(parse_positive("abc").is_err());
}
#[test]
fn test_negative() {
assert!(parse_positive("-5").is_err());
}
#[test]
fn test_zero() {
assert!(parse_positive("0").is_err());
}
}
Bài 3: Test Struct
struct BankAccount {
balance: f64,
}
impl BankAccount {
fn new(initial_balance: f64) -> Self {
BankAccount { balance: initial_balance }
}
fn deposit(&mut self, amount: f64) -> Result<(), String> {
if amount <= 0.0 {
return Err(String::from("Amount must be positive"));
}
self.balance += amount;
Ok(())
}
fn withdraw(&mut self, amount: f64) -> Result<(), String> {
if amount <= 0.0 {
return Err(String::from("Amount must be positive"));
}
if amount > self.balance {
return Err(String::from("Insufficient funds"));
}
self.balance -= amount;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
// TODO: Test new account
// TODO: Test deposit
// TODO: Test withdraw
// TODO: Test insufficient funds
}
💡 Gợi ý
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_account() {
let account = BankAccount::new(100.0);
assert_eq!(account.balance, 100.0);
}
#[test]
fn test_deposit() {
let mut account = BankAccount::new(100.0);
account.deposit(50.0).unwrap();
assert_eq!(account.balance, 150.0);
}
#[test]
fn test_withdraw() {
let mut account = BankAccount::new(100.0);
account.withdraw(30.0).unwrap();
assert_eq!(account.balance, 70.0);
}
#[test]
fn test_insufficient_funds() {
let mut account = BankAccount::new(100.0);
assert!(account.withdraw(150.0).is_err());
}
#[test]
fn test_negative_deposit() {
let mut account = BankAccount::new(100.0);
assert!(account.deposit(-50.0).is_err());
}
}
🎯 Tóm Tắt
| Macro | Mô Tả | Example |
|---|---|---|
#[test] | Đánh dấu test function | #[test] fn test() {} |
assert! | Assert condition true | assert!(x > 0) |
assert_eq! | Assert equality | assert_eq!(x, 5) |
assert_ne! | Assert inequality | assert_ne!(x, y) |
#[should_panic] | Expect panic | #[should_panic] |
#[ignore] | Ignore test | #[ignore] |
Quy tắc vàng:
- ✅ Viết tests cho mọi function public
- ✅ Test cả success và error cases
- ✅ Dùng descriptive test names
- ✅ Tổ chức tests trong modules
- ✅ Run tests thường xuyên
- ✅ Tests là documentation
🔗 Liên Kết Hữu Ích
Bài tiếp theo: Integration Testing →
Trong bài tiếp theo, chúng ta sẽ tìm hiểu về Integration Testing - test toàn bộ crate!