Skip to content

Advanced Testing QA & Security

Nếu unit tests là khóa cửa, thì fuzzing và property tests là security audit

Testing Pyramid trong Rust

                    ┌─────────────┐
                    │   Manual    │  ← Exploratory, UI
                   /│   Testing   │\
                  / └─────────────┘ \
                 /                   \
                /   ┌─────────────┐   \
               /    │ Integration │    \   ← API contracts, DB
              /     │    Tests    │     \
             /      └─────────────┘      \
            /                             \
           /        ┌─────────────┐        \
          /         │  Property   │         \  ← Proptest, Fuzzing
         /          │    Tests    │          \
        /           └─────────────┘           \
       /                                       \
      /            ┌─────────────────┐          \
     /             │   Unit Tests    │           \  ← Standard #[test]
    /              └─────────────────┘            \
   ───────────────────────────────────────────────

1. Property-Based Testing với proptest

Ý tưởng: Thay vì viết test cases thủ công, ta mô tả thuộc tính mà code phải thỏa mãn. Library tự generate hàng nghìn inputs.

Setup

toml
[dev-dependencies]
proptest = "1"

Ví dụ: Test reverse function

rust
use proptest::prelude::*;

fn reverse<T: Clone>(xs: &[T]) -> Vec<T> {
    xs.iter().cloned().rev().collect()
}

proptest! {
    #[test]
    fn test_reverse_twice_is_identity(xs: Vec<i32>) {
        // Property: reverse(reverse(x)) == x
        let result = reverse(&reverse(&xs));
        prop_assert_eq!(result, xs);
    }
    
    #[test]
    fn test_reverse_preserves_length(xs: Vec<i32>) {
        // Property: length không thay đổi
        prop_assert_eq!(reverse(&xs).len(), xs.len());
    }
    
    #[test]
    fn test_reverse_first_becomes_last(xs: Vec<i32>) {
        // Property: phần tử đầu trở thành phần tử cuối
        prop_assume!(!xs.is_empty());  // Skip empty vectors
        let reversed = reverse(&xs);
        prop_assert_eq!(xs.first(), reversed.last());
    }
}

Custom Strategies

rust
use proptest::prelude::*;

// Generate valid email addresses
fn email_strategy() -> impl Strategy<Value = String> {
    (
        "[a-z]{3,10}",                    // local part
        "[a-z]{3,8}",                     // domain name
        prop_oneof!["com", "org", "net"], // TLD
    )
    .prop_map(|(local, domain, tld)| format!("{}@{}.{}", local, domain, tld))
}

// Generate valid User structs
fn user_strategy() -> impl Strategy<Value = User> {
    (
        email_strategy(),
        "[A-Z][a-z]{2,15}",  // Name starting with capital
        18..100u8,            // Age
    )
    .prop_map(|(email, name, age)| User { email, name, age })
}

proptest! {
    #[test]
    fn test_user_serialization_roundtrip(user in user_strategy()) {
        let json = serde_json::to_string(&user).unwrap();
        let parsed: User = serde_json::from_str(&json).unwrap();
        prop_assert_eq!(user, parsed);
    }
}

Test Sorting Algorithm

rust
proptest! {
    #[test]
    fn test_sort_is_sorted(mut xs: Vec<i32>) {
        xs.sort();
        
        // Property 1: Output is sorted
        prop_assert!(xs.windows(2).all(|w| w[0] <= w[1]));
    }
    
    #[test]
    fn test_sort_is_permutation(xs: Vec<i32>) {
        let mut sorted = xs.clone();
        sorted.sort();
        
        // Property 2: Same elements, different order
        let mut original_sorted = xs.clone();
        original_sorted.sort();
        prop_assert_eq!(sorted, original_sorted);
    }
    
    #[test]
    fn test_sort_idempotent(mut xs: Vec<i32>) {
        xs.sort();
        let first_sort = xs.clone();
        xs.sort();
        
        // Property 3: Sorting twice == Sorting once
        prop_assert_eq!(xs, first_sort);
    }
}

2. Fuzzing với cargo-fuzz

Ý tưởng: Tự động generate random inputs để tìm crashes, panics, hoặc undefined behavior.

Setup

bash
cargo install cargo-fuzz

# Trong project (yêu cầu nightly)
cargo +nightly fuzz init

Fuzz Target

rust
// fuzz/fuzz_targets/parse_json.rs
#![no_main]

use libfuzzer_sys::fuzz_target;
use my_crate::parse_config;

fuzz_target!(|data: &[u8]| {
    // Fuzzer sẽ generate bytes ngẫu nhiên
    if let Ok(s) = std::str::from_utf8(data) {
        // Bỏ qua kết quả, chỉ quan tâm crashes
        let _ = parse_config(s);
    }
});

Structured Fuzzing với Arbitrary

rust
use arbitrary::Arbitrary;

#[derive(Debug, Arbitrary)]
struct FuzzInput {
    name: String,
    count: u32,
    enabled: bool,
}

fuzz_target!(|input: FuzzInput| {
    // Fuzzer generate structured data
    let _ = process_input(&input);
});

Run Fuzzer

bash
# Chạy liên tục (dừng bằng Ctrl+C)
cargo +nightly fuzz run parse_json

# Chạy với giới hạn thời gian
cargo +nightly fuzz run parse_json -- -max_total_time=300

# Chạy với corpus seeds
cargo +nightly fuzz run parse_json fuzz/corpus/parse_json/

Analyze Crashes

bash
# Xem crash details
cargo +nightly fuzz fmt parse_json crash-*

# Minimize crash input
cargo +nightly fuzz tmin parse_json crash-*

3. Mocking trong Rust

Rust không có reflection, nên mocking phức tạp hơn Java/Python. Có 3 approaches:

Approach 1: Trait + Test Double

rust
// Production trait
trait EmailSender: Send + Sync {
    fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), Error>;
}

// Production implementation
struct SmtpEmailSender { /* ... */ }
impl EmailSender for SmtpEmailSender {
    fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), Error> {
        // Actual SMTP logic
    }
}

// Test double
#[cfg(test)]
struct MockEmailSender {
    sent_emails: std::sync::Mutex<Vec<(String, String, String)>>,
}

#[cfg(test)]
impl MockEmailSender {
    fn new() -> Self {
        Self { sent_emails: Mutex::new(vec![]) }
    }
    
    fn get_sent(&self) -> Vec<(String, String, String)> {
        self.sent_emails.lock().unwrap().clone()
    }
}

#[cfg(test)]
impl EmailSender for MockEmailSender {
    fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), Error> {
        self.sent_emails.lock().unwrap().push((
            to.to_string(),
            subject.to_string(),
            body.to_string(),
        ));
        Ok(())
    }
}

#[test]
fn test_user_registration_sends_welcome_email() {
    let mock_sender = Arc::new(MockEmailSender::new());
    let service = UserService::new(mock_sender.clone());
    
    service.register("test@example.com", "password").unwrap();
    
    let sent = mock_sender.get_sent();
    assert_eq!(sent.len(), 1);
    assert_eq!(sent[0].0, "test@example.com");
    assert!(sent[0].1.contains("Welcome"));
}

Approach 2: mockall crate

toml
[dev-dependencies]
mockall = "0.12"
rust
use mockall::{automock, predicate::*};

#[automock]  // Generates MockDatabase automatically
trait Database {
    fn get_user(&self, id: u64) -> Option<User>;
    fn save_user(&self, user: &User) -> Result<(), DbError>;
}

#[test]
fn test_get_existing_user() {
    let mut mock = MockDatabase::new();
    
    // Setup expectations
    mock.expect_get_user()
        .with(eq(42))
        .times(1)
        .returning(|_| Some(User { id: 42, name: "Alice".into() }));
    
    let service = UserService::new(Box::new(mock));
    let user = service.find_user(42).unwrap();
    
    assert_eq!(user.name, "Alice");
}

#[test]
fn test_save_user_called_once() {
    let mut mock = MockDatabase::new();
    
    mock.expect_save_user()
        .with(function(|u: &User| u.name == "Bob"))
        .times(1)
        .returning(|_| Ok(()));
    
    let service = UserService::new(Box::new(mock));
    service.create_user("Bob").unwrap();
    
    // mockall automatically verifies expectations on drop
}

Approach 3: Feature Flags cho Test Implementations

toml
# Cargo.toml
[features]
default = []
test-mocks = []
rust
#[cfg(not(feature = "test-mocks"))]
mod real {
    pub struct HttpClient { /* actual implementation */ }
}

#[cfg(feature = "test-mocks")]
mod mocked {
    pub struct HttpClient {
        pub responses: std::collections::HashMap<String, String>,
    }
    
    impl HttpClient {
        pub fn with_responses(responses: Vec<(&str, &str)>) -> Self {
            Self {
                responses: responses.into_iter()
                    .map(|(k, v)| (k.to_string(), v.to_string()))
                    .collect()
            }
        }
    }
}

#[cfg(not(feature = "test-mocks"))]
pub use real::HttpClient;

#[cfg(feature = "test-mocks")]
pub use mocked::HttpClient;

4. Test Organization Best Practices

Separate Test Crate for Integration Tests

my_app/
├── src/
│   └── lib.rs
├── tests/                    # Integration tests
│   ├── common/
│   │   └── mod.rs           # Shared test utilities
│   ├── api_tests.rs
│   └── db_tests.rs
└── Cargo.toml

Test Fixtures with rstest

toml
[dev-dependencies]
rstest = "0.18"
rust
use rstest::{fixture, rstest};

#[fixture]
fn database() -> TestDatabase {
    TestDatabase::new().with_seed_data()
}

#[fixture]
fn user() -> User {
    User { id: 1, name: "Test".into(), email: "test@example.com".into() }
}

#[rstest]
fn test_user_can_be_saved(database: TestDatabase, user: User) {
    database.save(&user).unwrap();
    assert!(database.exists(user.id));
}

#[rstest]
#[case::admin(Role::Admin, true)]
#[case::user(Role::User, false)]
#[case::guest(Role::Guest, false)]
fn test_permission_check(#[case] role: Role, #[case] expected: bool) {
    let user = User { role, ..Default::default() };
    assert_eq!(user.can_delete_posts(), expected);
}
)

Bảng Tóm tắt

TechniqueCrateBest For
Unit TestsBuilt-in #[test]Function-level logic
Property TestsproptestAlgorithms, parsers, serialization
Fuzzingcargo-fuzzSecurity, crash discovery
MockingmockallExternal dependencies
FixturesrstestTest data setup