Chuyển đến nội dung chính

Lesson 19: Testing & Benchmarking

Unit tests, integration tests, doc tests. rstest (fixtures, parametrize), mockall. API testing with reqwest. Criterion.rs benchmarking. Property-based testing, fuzzing. Code coverage.

💻 Programming — Lesson 19 Lesson 19: Testing & Benchmarking

Rust: From Basics to Advanced

Part 6: Testing, CI/CD & Production

xdev.asia

1. Unit Tests

pub fn add(a: i32, b: i32) -> i32 { a + b }

pub fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 { Err("Division by zero".into()) } else { Ok(a / b) }
}

#[cfg(test)]
mod tests {
    use super::*;

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

    #[test]
    fn test_divide() {
        assert_eq!(divide(10.0, 2.0), Ok(5.0));
    }

    #[test]
    fn test_divide_by_zero() {
        assert!(divide(10.0, 0.0).is_err());
    }

    #[test]
    #[should_panic(expected = "overflow")]
    fn test_overflow() {
        let _ = i8::MAX.checked_add(1).expect("overflow");
    }
}

2. rstest — Fixtures & Parametrize

use rstest::*;

#[fixture]
fn db_pool() -> PgPool {
    // Setup test database
    PgPoolOptions::new().connect_lazy("postgres://test:test@localhost/test").unwrap()
}

#[rstest]
#[case(2, 3, 5)]
#[case(0, 0, 0)]
#[case(-1, 1, 0)]
fn test_add_parametrized(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
    assert_eq!(add(a, b), expected);
}

// Async test
#[rstest]
#[tokio::test]
async fn test_create_product(db_pool: PgPool) {
    let repo = ProductRepository::new(db_pool);
    let product = repo.create("Test", 9.99, None).await.unwrap();
    assert_eq!(product.name, "Test");
}

3. Mocking with mockall

use mockall::{automock, predicate::*};

#[automock]
trait UserRepository {
    async fn find_by_id(&self, id: &str) -> Option<User>;
    async fn save(&self, user: &User) -> Result<(), String>;
}

#[tokio::test]
async fn test_user_service() {
    let mut mock = MockUserRepository::new();

    mock.expect_find_by_id()
        .with(eq("user-1"))
        .times(1)
        .returning(|_| Some(User { id: "user-1".into(), name: "Alice".into() }));

    let service = UserService::new(mock);
    let user = service.get_user("user-1").await.unwrap();
    assert_eq!(user.name, "Alice");
}

4. Criterion Benchmarking

// benches/my_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn fibonacci(n: u64) -> u64 {
    match n {
        0 | 1 => n,
        _ => { let (mut a, mut b) = (0, 1); for _ in 2..=n { let t = a + b; a = b; b = t; } b }
    }
}

fn bench_fibonacci(c: &mut Criterion) {
    c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));

    let mut group = c.benchmark_group("fibonacci");
    for n in [10, 20, 30, 40] {
        group.bench_with_input(format!("fib {}", n), &n, |b, &n| {
            b.iter(|| fibonacci(black_box(n)));
        });
    }
    group.finish();
}

criterion_group!(benches, bench_fibonacci);
criterion_main!(benches);
cargo bench
cargo test
cargo test -- --test-threads=1  # Sequential
cargo tarpaulin --out Html       # Coverage

Next article: Docker & CI/CD for Rust.