471 lines
10 KiB
Markdown
471 lines
10 KiB
Markdown
# Testing in Rust
|
|
|
|
## Unit Tests
|
|
|
|
```rust
|
|
// Tests in same file
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_addition() {
|
|
assert_eq!(2 + 2, 4);
|
|
}
|
|
|
|
#[test]
|
|
fn test_subtraction() {
|
|
assert!(10 - 5 == 5);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "division by zero")]
|
|
fn test_panic() {
|
|
divide(10, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_result() -> Result<(), String> {
|
|
let result = divide(10, 2)?;
|
|
assert_eq!(result, 5);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn expensive_test() {
|
|
// Run with: cargo test -- --ignored
|
|
}
|
|
}
|
|
|
|
// Assertions
|
|
fn assert_examples() {
|
|
assert!(true);
|
|
assert_eq!(2 + 2, 4);
|
|
assert_ne!(2 + 2, 5);
|
|
|
|
// Custom messages
|
|
assert!(value > 0, "Value must be positive, got {}", value);
|
|
assert_eq!(result, expected, "Calculation failed");
|
|
}
|
|
```
|
|
|
|
## Doctests
|
|
|
|
```rust
|
|
/// Adds two numbers together.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use mylib::add;
|
|
///
|
|
/// let result = add(2, 3);
|
|
/// assert_eq!(result, 5);
|
|
/// ```
|
|
///
|
|
/// ```should_panic
|
|
/// use mylib::divide;
|
|
///
|
|
/// divide(10, 0); // This will panic
|
|
/// ```
|
|
///
|
|
/// ```ignore
|
|
/// // This code won't compile but won't fail the test
|
|
/// let x = undefined_function();
|
|
/// ```
|
|
pub fn add(a: i32, b: i32) -> i32 {
|
|
a + b
|
|
}
|
|
```
|
|
|
|
## Integration Tests
|
|
|
|
```rust
|
|
// tests/integration_test.rs
|
|
use mylib;
|
|
|
|
#[test]
|
|
fn test_full_workflow() {
|
|
let config = mylib::Config::new("test.conf");
|
|
let result = mylib::process(&config);
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
// tests/common/mod.rs - shared test utilities
|
|
pub fn setup() -> TestContext {
|
|
TestContext {
|
|
db: create_test_db(),
|
|
}
|
|
}
|
|
|
|
// tests/another_test.rs
|
|
mod common;
|
|
|
|
#[test]
|
|
fn test_with_common() {
|
|
let ctx = common::setup();
|
|
// Use ctx...
|
|
}
|
|
```
|
|
|
|
## Test Organization
|
|
|
|
```rust
|
|
// Nested test modules
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
mod addition {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn positive_numbers() {
|
|
assert_eq!(add(2, 3), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn negative_numbers() {
|
|
assert_eq!(add(-2, -3), -5);
|
|
}
|
|
}
|
|
|
|
mod subtraction {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_subtract() {
|
|
assert_eq!(subtract(10, 5), 5);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Test Fixtures and Setup
|
|
|
|
```rust
|
|
struct TestContext {
|
|
temp_dir: std::path::PathBuf,
|
|
db: Database,
|
|
}
|
|
|
|
impl TestContext {
|
|
fn setup() -> Self {
|
|
let temp_dir = std::env::temp_dir().join("test");
|
|
std::fs::create_dir_all(&temp_dir).unwrap();
|
|
|
|
Self {
|
|
temp_dir,
|
|
db: Database::connect_test(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for TestContext {
|
|
fn drop(&mut self) {
|
|
// Cleanup
|
|
std::fs::remove_dir_all(&self.temp_dir).ok();
|
|
self.db.disconnect();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_with_fixture() {
|
|
let ctx = TestContext::setup();
|
|
// Test uses ctx...
|
|
// Automatic cleanup via Drop
|
|
}
|
|
```
|
|
|
|
## Async Tests
|
|
|
|
```rust
|
|
use tokio;
|
|
|
|
#[tokio::test]
|
|
async fn test_async_function() {
|
|
let result = async_operation().await;
|
|
assert_eq!(result, 42);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_with_custom_runtime() {
|
|
let result = concurrent_operation().await;
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
// Testing async with timeout
|
|
#[tokio::test]
|
|
async fn test_with_timeout() {
|
|
let timeout = std::time::Duration::from_secs(5);
|
|
let result = tokio::time::timeout(timeout, slow_operation()).await;
|
|
assert!(result.is_ok());
|
|
}
|
|
```
|
|
|
|
## Property-Based Testing (proptest)
|
|
|
|
```rust
|
|
use proptest::prelude::*;
|
|
|
|
// Simple property test
|
|
proptest! {
|
|
#[test]
|
|
fn test_reversing_twice_is_identity(ref s in ".*") {
|
|
let reversed: String = s.chars().rev().collect();
|
|
let double_reversed: String = reversed.chars().rev().collect();
|
|
assert_eq!(s, &double_reversed);
|
|
}
|
|
}
|
|
|
|
// Custom strategies
|
|
proptest! {
|
|
#[test]
|
|
fn test_addition_commutative(a in 0..1000i32, b in 0..1000i32) {
|
|
assert_eq!(a + b, b + a);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vector_push_pop(
|
|
ref v in prop::collection::vec(0..100i32, 0..100),
|
|
item in 0..100i32
|
|
) {
|
|
let mut v = v.clone();
|
|
v.push(item);
|
|
assert_eq!(v.pop(), Some(item));
|
|
}
|
|
}
|
|
|
|
// Complex custom strategies
|
|
fn user_strategy() -> impl Strategy<Value = User> {
|
|
(1..1000u64, "[a-z]{3,10}", "[a-z0-9.]+@[a-z]+\\.[a-z]+")
|
|
.prop_map(|(id, name, email)| User { id, name, email })
|
|
}
|
|
|
|
proptest! {
|
|
#[test]
|
|
fn test_user_serialization(user in user_strategy()) {
|
|
let json = serde_json::to_string(&user).unwrap();
|
|
let deserialized: User = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(user, deserialized);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Mocking
|
|
|
|
```rust
|
|
// Using mockall
|
|
use mockall::*;
|
|
use mockall::predicate::*;
|
|
|
|
#[automock]
|
|
trait Database {
|
|
fn get_user(&self, id: u64) -> Option<User>;
|
|
fn save_user(&mut self, user: User) -> Result<(), Error>;
|
|
}
|
|
|
|
#[test]
|
|
fn test_with_mock() {
|
|
let mut mock = MockDatabase::new();
|
|
|
|
mock.expect_get_user()
|
|
.with(eq(1))
|
|
.times(1)
|
|
.returning(|_| Some(User { id: 1, name: "Alice".to_string() }));
|
|
|
|
mock.expect_save_user()
|
|
.times(1)
|
|
.returning(|_| Ok(()));
|
|
|
|
// Use mock in test
|
|
let user = mock.get_user(1);
|
|
assert!(user.is_some());
|
|
}
|
|
```
|
|
|
|
## Benchmarks (Criterion)
|
|
|
|
```rust
|
|
// benches/my_benchmark.rs
|
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
|
|
|
fn fibonacci(n: u64) -> u64 {
|
|
match n {
|
|
0 => 1,
|
|
1 => 1,
|
|
n => fibonacci(n - 1) + fibonacci(n - 2),
|
|
}
|
|
}
|
|
|
|
fn criterion_benchmark(c: &mut Criterion) {
|
|
c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));
|
|
}
|
|
|
|
criterion_group!(benches, criterion_benchmark);
|
|
criterion_main!(benches);
|
|
|
|
// Cargo.toml:
|
|
// [dev-dependencies]
|
|
// criterion = "0.5"
|
|
//
|
|
// [[bench]]
|
|
// name = "my_benchmark"
|
|
// harness = false
|
|
```
|
|
|
|
## Advanced Benchmarking
|
|
|
|
```rust
|
|
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
|
|
|
|
fn bench_multiple_sizes(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("sorting");
|
|
|
|
for size in [10, 100, 1000, 10000].iter() {
|
|
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
|
|
b.iter_batched(
|
|
|| generate_random_vec(size),
|
|
|mut v| v.sort(),
|
|
criterion::BatchSize::SmallInput,
|
|
);
|
|
});
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// Comparing implementations
|
|
fn bench_comparison(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("string_search");
|
|
|
|
group.bench_function("naive", |b| {
|
|
b.iter(|| naive_search(black_box("haystack"), black_box("needle")))
|
|
});
|
|
|
|
group.bench_function("optimized", |b| {
|
|
b.iter(|| optimized_search(black_box("haystack"), black_box("needle")))
|
|
});
|
|
|
|
group.finish();
|
|
}
|
|
|
|
criterion_group!(benches, bench_multiple_sizes, bench_comparison);
|
|
criterion_main!(benches);
|
|
```
|
|
|
|
## Testing with External Resources
|
|
|
|
```rust
|
|
// Testing file I/O
|
|
#[test]
|
|
fn test_file_operations() {
|
|
use std::io::Write;
|
|
|
|
let temp_dir = std::env::temp_dir();
|
|
let file_path = temp_dir.join("test_file.txt");
|
|
|
|
// Write
|
|
let mut file = std::fs::File::create(&file_path).unwrap();
|
|
file.write_all(b"test content").unwrap();
|
|
|
|
// Read
|
|
let content = std::fs::read_to_string(&file_path).unwrap();
|
|
assert_eq!(content, "test content");
|
|
|
|
// Cleanup
|
|
std::fs::remove_file(&file_path).unwrap();
|
|
}
|
|
|
|
// Testing with databases (using sqlx)
|
|
#[sqlx::test]
|
|
async fn test_database_operations(pool: sqlx::PgPool) -> sqlx::Result<()> {
|
|
sqlx::query("INSERT INTO users (name) VALUES ($1)")
|
|
.bind("Alice")
|
|
.execute(&pool)
|
|
.await?;
|
|
|
|
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
|
|
.fetch_one(&pool)
|
|
.await?;
|
|
|
|
assert_eq!(count.0, 1);
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
## Snapshot Testing
|
|
|
|
```rust
|
|
// Using insta crate
|
|
use insta::assert_snapshot;
|
|
|
|
#[test]
|
|
fn test_output_format() {
|
|
let data = generate_complex_output();
|
|
assert_snapshot!(data);
|
|
}
|
|
|
|
#[test]
|
|
fn test_json_output() {
|
|
let json = serde_json::to_string_pretty(&get_data()).unwrap();
|
|
assert_snapshot!(json);
|
|
}
|
|
|
|
// Run with: cargo insta test
|
|
// Review snapshots: cargo insta review
|
|
```
|
|
|
|
## Code Coverage
|
|
|
|
```rust
|
|
// Using tarpaulin
|
|
// cargo install cargo-tarpaulin
|
|
// cargo tarpaulin --out Html --output-dir coverage
|
|
|
|
// Using llvm-cov
|
|
// cargo install cargo-llvm-cov
|
|
// cargo llvm-cov --html
|
|
```
|
|
|
|
## Fuzzing
|
|
|
|
```rust
|
|
// Using cargo-fuzz
|
|
// cargo install cargo-fuzz
|
|
// cargo fuzz init
|
|
|
|
// fuzz/fuzz_targets/fuzz_target_1.rs
|
|
#![no_main]
|
|
use libfuzzer_sys::fuzz_target;
|
|
|
|
fuzz_target!(|data: &[u8]| {
|
|
if let Ok(s) = std::str::from_utf8(data) {
|
|
let _ = mylib::parse_input(s);
|
|
}
|
|
});
|
|
|
|
// Run with: cargo fuzz run fuzz_target_1
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
- Write tests alongside production code in #[cfg(test)] modules
|
|
- Use integration tests in tests/ directory for end-to-end testing
|
|
- Include doctests in documentation for examples that must work
|
|
- Use descriptive test names that explain what is being tested
|
|
- Test edge cases (empty inputs, max values, etc.)
|
|
- Use property-based testing for algorithmic code
|
|
- Benchmark performance-critical code with criterion
|
|
- Run tests in CI with cargo test --all-features
|
|
- Use cargo test -- --nocapture to see println! output
|
|
- Test error conditions with #[should_panic] or Result
|
|
- Mock external dependencies for unit tests
|
|
- Use test fixtures for complex setup/teardown
|
|
- Run clippy on test code too
|
|
- Measure code coverage and aim for high coverage
|
|
- Use fuzzing for security-critical parsers
|
|
- Test async code with tokio::test
|
|
- Use snapshot testing for complex output validation
|