Giao diện
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 initFuzz 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.tomlTest 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
| Technique | Crate | Best For |
|---|---|---|
| Unit Tests | Built-in #[test] | Function-level logic |
| Property Tests | proptest | Algorithms, parsers, serialization |
| Fuzzing | cargo-fuzz | Security, crash discovery |
| Mocking | mockall | External dependencies |
| Fixtures | rstest | Test data setup |