Comprehensive testing is crucial for production code. This chapter covers advanced testing techniques including property testing, fuzzing, mocking, and more.
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_positive() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-2, -3), -5);
}
#[test]
#[should_panic(expected = "overflow")]
fn test_add_overflow() {
add(i32::MAX, 1);
}
#[test]
#[ignore]
fn expensive_test() {
// This test is ignored by default
// Run with: cargo test -- --ignored
}
}
// tests/integration_test.rs
#[test]
fn integration_test() {
// Integration tests go in separate files
}
macro_rules! assert_approx_eq {
($a:expr, $b:expr, $epsilon:expr) => {
let diff = ($a - $b).abs();
assert!(
diff < $epsilon,
"assertion failed: {} โ {} (diff: {})",
$a, $b, diff
);
};
}
#[test]
fn test_floating_point() {
assert_approx_eq!(0.1 + 0.2, 0.3, 1e-10);
}
// Using more_asserts crate
use more_asserts::{assert_gt, assert_le};
#[test]
fn test_ordering() {
assert_gt!(5, 3);
assert_le!(2, 5);
}
Property-based testing generates random inputs to verify properties that should always hold:
use proptest::prelude::*;
fn reverse<T: Clone>(vec: &[T]) -> Vec<T> {
vec.iter().rev().cloned().collect()
}
proptest! {
#[test]
fn test_reverse_twice_is_identity(vec in prop::collection::vec(any::<i32>(), 0..100)) {
let reversed_twice = reverse(&reverse(&vec));
prop_assert_eq!(vec, reversed_twice);
}
#[test]
fn test_reverse_length(vec in prop::collection::vec(any::<i32>(), 0..100)) {
let reversed = reverse(&vec);
prop_assert_eq!(vec.len(), reversed.len());
}
#[test]
fn test_addition_commutative(a in any::<i32>(), b in any::<i32>()) {
prop_assume!(a.checked_add(b).is_some()); // Avoid overflow
prop_assert_eq!(a + b, b + a);
}
}
use proptest::prelude::*;
#[derive(Debug, Clone)]
struct User {
name: String,
age: u8,
email: String,
}
fn user_strategy() -> impl Strategy<Value = User> {
(
"[a-z]{3,10}", // name: 3-10 lowercase letters
1u8..100, // age: 1-99
"[a-z]{5,10}@[a-z]{3,7}\\.com", // email
)
.prop_map(|(name, age, email)| User { name, age, email })
}
proptest! {
#[test]
fn test_user_age_valid(user in user_strategy()) {
prop_assert!(user.age > 0 && user.age < 100);
}
#[test]
fn test_user_email_format(user in user_strategy()) {
prop_assert!(user.email.contains('@'));
prop_assert!(user.email.ends_with(".com"));
}
}
Fuzzing automatically generates inputs to find crashes and bugs:
// Install: cargo install cargo-fuzz
// Create fuzzer: cargo fuzz init
// fuzz/fuzz_targets/fuzz_parser.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
if let Ok(s) = std::str::from_utf8(data) {
// Fuzz your parser
let _ = my_parser::parse(s);
}
});
// Run: cargo fuzz run fuzz_parser
// Structured fuzzing with arbitrary
use arbitrary::Arbitrary;
#[derive(Debug, Arbitrary)]
struct FuzzInput {
action: Action,
value: i32,
}
#[derive(Debug, Arbitrary)]
enum Action {
Add,
Subtract,
Multiply,
}
fuzz_target!(|input: FuzzInput| {
match input.action {
Action::Add => { /* test add */ }
Action::Subtract => { /* test subtract */ }
Action::Multiply => { /* test multiply */ }
}
});
trait Database {
fn get_user(&self, id: u64) -> Option<String>;
fn save_user(&mut self, id: u64, name: String) -> Result<(), String>;
}
struct MockDatabase {
users: std::collections::HashMap<u64, String>,
}
impl Database for MockDatabase {
fn get_user(&self, id: u64) -> Option<String> {
self.users.get(&id).cloned()
}
fn save_user(&mut self, id: u64, name: String) -> Result<(), String> {
self.users.insert(id, name);
Ok(())
}
}
#[test]
fn test_user_service() {
let mut db = MockDatabase {
users: std::collections::HashMap::new(),
};
db.save_user(1, "Alice".to_string()).unwrap();
assert_eq!(db.get_user(1), Some("Alice".to_string()));
}
use mockall::*;
#[automock]
trait Repository {
fn find_by_id(&self, id: u64) -> Option<String>;
fn save(&mut self, id: u64, data: String);
}
#[test]
fn test_with_mock() {
let mut mock = MockRepository::new();
// Set expectations
mock.expect_find_by_id()
.with(eq(1))
.times(1)
.returning(|_| Some("Alice".to_string()));
mock.expect_save()
.with(eq(2), eq("Bob".to_string()))
.times(1)
.returning(|_, _| ());
// Use the mock
assert_eq!(mock.find_by_id(1), Some("Alice".to_string()));
mock.save(2, "Bob".to_string());
}
use insta::assert_snapshot;
fn render_user(name: &str, age: u8) -> String {
format!(
r#"
<div class="user">
<h1>{}</h1>
<p>Age: {}</p>
</div>
"#,
name, age
)
}
#[test]
fn test_render_user() {
let output = render_user("Alice", 30);
assert_snapshot!(output);
}
// First run creates snapshot
// Subsequent runs compare against it
// Review changes: cargo insta review
// tests/api_test.rs
use axum::{Router, routing::get};
use tower::ServiceExt;
use http::{Request, StatusCode};
async fn hello() -> &'static str {
"Hello, World!"
}
fn app() -> Router {
Router::new().route("/", get(hello))
}
#[tokio::test]
async fn test_hello_endpoint() {
let app = app();
let response = app
.oneshot(Request::builder().uri("/").body(()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body())
.await
.unwrap();
assert_eq!(&body[..], b"Hello, World!");
}
use sqlx::{PgPool, postgres::PgPoolOptions};
async fn setup_test_db() -> PgPool {
let pool = PgPoolOptions::new()
.max_connections(5)
.connect("postgres://localhost/test_db")
.await
.unwrap();
// Run migrations
sqlx::migrate!("./migrations")
.run(&pool)
.await
.unwrap();
pool
}
async fn cleanup_test_db(pool: &PgPool) {
sqlx::query("TRUNCATE TABLE users")
.execute(pool)
.await
.unwrap();
}
#[tokio::test]
async fn test_create_user() {
let pool = setup_test_db().await;
sqlx::query("INSERT INTO users (name, email) VALUES ($1, $2)")
.bind("Alice")
.bind("alice@example.com")
.execute(&pool)
.await
.unwrap();
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count.0, 1);
cleanup_test_db(&pool).await;
}
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
fn fibonacci(n: u64) -> u64 {
match n {
0 | 1 => 1,
n => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn criterion_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("fibonacci");
for i in [10u64, 15, 20].iter() {
group.bench_with_input(BenchmarkId::from_parameter(i), i, |b, &i| {
b.iter(|| fibonacci(black_box(i)));
});
}
group.finish();
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
use tokio::time::{sleep, Duration};
use std::sync::Arc;
use tokio::sync::Semaphore;
#[tokio::test]
async fn load_test_api() {
let semaphore = Arc::new(Semaphore::new(100)); // Max 100 concurrent
let mut handles = vec![];
for i in 0..1000 {
let permit = semaphore.clone().acquire_owned().await.unwrap();
let handle = tokio::spawn(async move {
let _permit = permit; // Hold permit
// Make request
let response = make_api_request(i).await;
assert!(response.is_ok());
});
handles.push(handle);
}
for handle in handles {
handle.await.unwrap();
}
}
async fn make_api_request(id: u64) -> Result<(), String> {
sleep(Duration::from_millis(10)).await;
Ok(())
}
struct TestContext {
db: MockDatabase,
config: Config,
}
impl TestContext {
fn new() -> Self {
TestContext {
db: MockDatabase::new(),
config: Config::default(),
}
}
fn with_user(mut self, id: u64, name: &str) -> Self {
self.db.users.insert(id, name.to_string());
self
}
}
#[test]
fn test_with_fixture() {
let ctx = TestContext::new()
.with_user(1, "Alice")
.with_user(2, "Bob");
assert_eq!(ctx.db.get_user(1), Some("Alice".to_string()));
}
mod test_helpers {
pub fn create_test_user(name: &str) -> User {
User {
id: 1,
name: name.to_string(),
email: format!("{}@example.com", name.to_lowercase()),
}
}
pub fn assert_user_eq(a: &User, b: &User) {
assert_eq!(a.id, b.id);
assert_eq!(a.name, b.name);
assert_eq!(a.email, b.email);
}
}
#[test]
fn test_user_creation() {
let user = test_helpers::create_test_user("Alice");
assert_eq!(user.name, "Alice");
}
// Install tarpaulin
// cargo install cargo-tarpaulin
// Run coverage
// cargo tarpaulin --out Html --output-dir coverage
// Or use llvm-cov (built-in)
// rustup component add llvm-tools-preview
// cargo install cargo-llvm-cov
// Run coverage
// cargo llvm-cov --html
// Example: achieving high coverage
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err("Division by zero".to_string())
} else {
Ok(a / b)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_divide_success() {
assert_eq!(divide(10, 2), Ok(5));
}
#[test]
fn test_divide_by_zero() {
assert_eq!(divide(10, 0), Err("Division by zero".to_string()));
}
// 100% coverage achieved!
}