Chuyển tới nội dung chính

🔗 Integration Testing

🎯 Mục Tiêu Bài Học

Sau khi hoàn thành bài học này, bạn sẽ:

  • ✅ Hiểu integration tests vs unit tests
  • ✅ Tạo tests directory
  • ✅ Test public API của crate
  • ✅ Tổ chức integration tests
  • ✅ Share code giữa integration tests
  • ✅ Test binary crates

🤔 Integration Tests Là Gì?

Ẩn Dụ Cuộc Sống: Kiểm Tra Tổng Thể

Integration Tests giống như test toàn bộ sản phẩm:

🏭 Unit Tests:

  • Test từng bộ phận riêng lẻ
  • Trong nội bộ module
  • Private functions

🔗 Integration Tests:

  • Test toàn bộ sản phẩm
  • Từ góc nhìn người dùng
  • Chỉ public API

So Sánh

FeatureUnit TestsIntegration Tests
LocationTrong src/Trong tests/
AccessPrivate + PublicChỉ Public
ScopeSingle moduleWhole crate
PurposeInternal logicExternal API

📁 Tests Directory

Cấu Trúc Project

my_project/
├── Cargo.toml
├── src/
│ └── lib.rs
└── tests/
├── integration_test.rs
└── common/
└── mod.rs

Example Library

// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}

pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}

pub struct Calculator {
value: i32,
}

impl Calculator {
pub fn new() -> Self {
Calculator { value: 0 }
}

pub fn add(&mut self, n: i32) {
self.value += n;
}

pub fn get(&self) -> i32 {
self.value
}
}

Integration Test

// tests/integration_test.rs
use my_project::{add, multiply, Calculator};

#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}

#[test]
fn test_multiply() {
assert_eq!(multiply(2, 3), 6);
}

#[test]
fn test_calculator() {
let mut calc = Calculator::new();
calc.add(5);
calc.add(3);
assert_eq!(calc.get(), 8);
}

🎯 Ví Dụ Thực Tế

Ví Dụ 1: String Utils Library

// src/lib.rs
pub fn reverse(s: &str) -> String {
s.chars().rev().collect()
}

pub fn is_palindrome(s: &str) -> bool {
let cleaned = s.to_lowercase().replace(" ", "");
cleaned == reverse(&cleaned)
}

pub fn count_vowels(s: &str) -> usize {
s.chars()
.filter(|c| "aeiouAEIOU".contains(*c))
.count()
}
// tests/string_tests.rs
use my_string_utils::*;

#[test]
fn test_reverse() {
assert_eq!(reverse("hello"), "olleh");
assert_eq!(reverse("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_vowels() {
assert_eq!(count_vowels("hello"), 2);
assert_eq!(count_vowels("aeiou"), 5);
assert_eq!(count_vowels("xyz"), 0);
}

Ví Dụ 2: Math Library

// src/lib.rs
pub fn factorial(n: u64) -> u64 {
match n {
0 | 1 => 1,
_ => n * factorial(n - 1),
}
}

pub fn fibonacci(n: u64) -> u64 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}

pub fn is_prime(n: u64) -> bool {
if n < 2 {
return false;
}
for i in 2..=(n as f64).sqrt() as u64 {
if n % i == 0 {
return false;
}
}
true
}
// tests/math_tests.rs
use my_math::*;

#[test]
fn test_factorial() {
assert_eq!(factorial(0), 1);
assert_eq!(factorial(1), 1);
assert_eq!(factorial(5), 120);
assert_eq!(factorial(10), 3628800);
}

#[test]
fn test_fibonacci() {
assert_eq!(fibonacci(0), 0);
assert_eq!(fibonacci(1), 1);
assert_eq!(fibonacci(10), 55);
}

#[test]
fn test_is_prime() {
assert!(is_prime(2));
assert!(is_prime(17));
assert!(!is_prime(4));
assert!(!is_prime(1));
}

Ví Dụ 3: User Management

// src/lib.rs
pub struct User {
pub username: String,
pub email: String,
}

pub struct UserManager {
users: Vec<User>,
}

impl UserManager {
pub fn new() -> Self {
UserManager { users: vec![] }
}

pub fn add_user(&mut self, username: String, email: String) -> Result<(), String> {
if self.users.iter().any(|u| u.username == username) {
return Err(String::from("Username already exists"));
}
self.users.push(User { username, email });
Ok(())
}

pub fn get_user(&self, username: &str) -> Option<&User> {
self.users.iter().find(|u| u.username == username)
}

pub fn user_count(&self) -> usize {
self.users.len()
}
}
// tests/user_tests.rs
use my_app::*;

#[test]
fn test_add_user() {
let mut manager = UserManager::new();
let result = manager.add_user("alice".to_string(), "[email protected]".to_string());
assert!(result.is_ok());
assert_eq!(manager.user_count(), 1);
}

#[test]
fn test_duplicate_username() {
let mut manager = UserManager::new();
manager.add_user("alice".to_string(), "[email protected]".to_string()).unwrap();
let result = manager.add_user("alice".to_string(), "[email protected]".to_string());
assert!(result.is_err());
}

#[test]
fn test_get_user() {
let mut manager = UserManager::new();
manager.add_user("alice".to_string(), "[email protected]".to_string()).unwrap();

let user = manager.get_user("alice");
assert!(user.is_some());
assert_eq!(user.unwrap().email, "[email protected]");
}

#[test]
fn test_get_nonexistent_user() {
let manager = UserManager::new();
let user = manager.get_user("bob");
assert!(user.is_none());
}

📚 Common Test Utilities

Shared Test Code

// tests/common/mod.rs
pub fn setup() -> TestContext {
TestContext {
// Setup test environment
}
}

pub struct TestContext {
// Test data
}

impl TestContext {
pub fn cleanup(&self) {
// Cleanup after test
}
}
// tests/integration_test.rs
mod common;

#[test]
fn test_with_common() {
let ctx = common::setup();

// Test code here

ctx.cleanup();
}

Example: Database Setup

// tests/common/mod.rs
use my_db::Database;

pub struct TestDb {
pub db: Database,
}

impl TestDb {
pub fn new() -> Self {
let db = Database::new_in_memory();
TestDb { db }
}

pub fn seed_data(&mut self) {
self.db.insert("key1", "value1");
self.db.insert("key2", "value2");
}
}

impl Drop for TestDb {
fn drop(&mut self) {
// Cleanup
}
}
// tests/db_tests.rs
mod common;
use common::TestDb;

#[test]
fn test_database_operations() {
let mut test_db = TestDb::new();
test_db.seed_data();

// Run tests
assert_eq!(test_db.db.get("key1"), Some("value1"));
}

🎯 Multiple Integration Test Files

Organizing Tests

tests/
├── string_tests.rs
├── math_tests.rs
├── user_tests.rs
└── common/
└── mod.rs
// tests/string_tests.rs
use my_lib::string_utils::*;

#[test]
fn test_strings() {
// String tests
}
// tests/math_tests.rs
use my_lib::math::*;

#[test]
fn test_math() {
// Math tests
}

Running Specific Test File

cargo test --test string_tests

🔧 Testing Binary Crates

Problem: Binaries Can't Be Imported

// src/main.rs
fn main() {
// Can't test this directly from integration tests
}

Solution: Extract to Library

my_project/
├── Cargo.toml
├── src/
│ ├── lib.rs # Library code
│ └── main.rs # Binary wrapper
└── tests/
└── integration_test.rs
// src/lib.rs
pub fn run() {
// Main logic here
}
// src/main.rs
use my_project;

fn main() {
my_project::run();
}
// tests/integration_test.rs
use my_project;

#[test]
fn test_run() {
// Test the library
}

🎯 Best Practices

1. Test Public API Only

// ✅ Good - testing public API
#[test]
fn test_public_function() {
assert_eq!(my_lib::public_function(), 42);
}

// ❌ Bad - can't access private
// #[test]
// fn test_private_function() {
// assert_eq!(my_lib::private_function(), 42); // ERROR
// }

2. One Feature Per File

// tests/authentication.rs
#[test]
fn test_login() { /* ... */ }

#[test]
fn test_logout() { /* ... */ }

// tests/database.rs
#[test]
fn test_insert() { /* ... */ }

#[test]
fn test_query() { /* ... */ }

3. Use Descriptive Names

#[test]
fn user_can_register_with_valid_credentials() {
// Test implementation
}

#[test]
fn registration_fails_with_invalid_email() {
// Test implementation
}

4. Setup và Teardown

mod common;

#[test]
fn test_with_setup() {
// Setup
let ctx = common::setup();

// Test
// ...

// Teardown (automatic with Drop)
}

💻 Bài Tập Thực Hành

Bài 1: Test Calculator Library

// src/lib.rs
pub struct Calculator {
value: f64,
}

impl Calculator {
pub fn new() -> Self {
Calculator { value: 0.0 }
}

pub fn add(&mut self, n: f64) -> &mut Self {
self.value += n;
self
}

pub fn subtract(&mut self, n: f64) -> &mut Self {
self.value -= n;
self
}

pub fn multiply(&mut self, n: f64) -> &mut Self {
self.value *= n;
self
}

pub fn result(&self) -> f64 {
self.value
}
}

// TODO: Viết integration tests trong tests/calculator_tests.rs
💡 Gợi ý
// tests/calculator_tests.rs
use my_calculator::Calculator;

#[test]
fn test_basic_operations() {
let mut calc = Calculator::new();
calc.add(10.0).subtract(3.0).multiply(2.0);
assert_eq!(calc.result(), 14.0);
}

#[test]
fn test_chain_operations() {
let result = Calculator::new()
.add(5.0)
.add(3.0)
.multiply(2.0)
.result();
assert_eq!(result, 16.0);
}

Bài 2: Test TodoList Library

// src/lib.rs
pub struct TodoList {
items: Vec<String>,
}

impl TodoList {
pub fn new() -> Self {
TodoList { items: vec![] }
}

pub fn add(&mut self, item: String) {
self.items.push(item);
}

pub fn remove(&mut self, index: usize) -> Option<String> {
if index < self.items.len() {
Some(self.items.remove(index))
} else {
None
}
}

pub fn get(&self, index: usize) -> Option<&String> {
self.items.get(index)
}

pub fn len(&self) -> usize {
self.items.len()
}
}

// TODO: Viết integration tests
💡 Gợi ý
// tests/todo_tests.rs
use my_todo::TodoList;

#[test]
fn test_add_items() {
let mut list = TodoList::new();
list.add("Task 1".to_string());
list.add("Task 2".to_string());
assert_eq!(list.len(), 2);
}

#[test]
fn test_remove_item() {
let mut list = TodoList::new();
list.add("Task 1".to_string());
let removed = list.remove(0);
assert_eq!(removed, Some("Task 1".to_string()));
assert_eq!(list.len(), 0);
}

#[test]
fn test_remove_invalid_index() {
let mut list = TodoList::new();
let removed = list.remove(10);
assert_eq!(removed, None);
}

🎯 Tóm Tắt

AspectUnit TestsIntegration Tests
Locationsrc/ with #[cfg(test)]tests/ directory
ScopeSingle moduleWhole crate
AccessPrivate + PublicPublic only
CompilePart of crateSeparate binary
PurposeInternal logicExternal API

Quy tắc vàng:

  • ✅ Integration tests trong tests/ directory
  • ✅ Mỗi file là một integration test
  • ✅ Chỉ test public API
  • ✅ Dùng common/ cho shared code
  • ✅ Tổ chức tests theo features
  • ✅ Test như end user sử dụng

🔗 Liên Kết Hữu Ích


Bài tiếp theo: Declarative Macros →

Trong bài tiếp theo, chúng ta sẽ tìm hiểu về Macros - viết code sinh code!

Loading comments...