Rust

****#[cfg(test)] mod tests { #[test] fn ... }``cargo test

**** tests/ crate API cargo test --test integration_test

****//! ```rust cargo test doc

****#[bench] nightly channel criterion crate

/>80%
100%
E2E
-

Mock mockall crate mock

:
:


cfg(test)

#[cfg(test)]

#[cfg(test)] cargo test


  1. **** pub(crate)

// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}
 
//  — 
fn validate_input(n: i32) -> bool {
    n >= 0 && n <= 1000
}
 
pub fn validated_add(a: i32, b: i32) -> Result<i32, String> {
    if validate_input(a) && validate_input(b) {
        Ok(a + b)
    } else {
        Err("input out of range".into())
    }
}
 
#[cfg(test)]
mod tests {
    use super::*;  // 
 
    #[test]
    fn test_add_basic() {
        assert_eq!(add(2, 3), 5);
    }
 
    //  — 
    #[test]
    fn test_validate_input() {
        assert!(validate_input(0));
        assert!(validate_input(500));
        assert!(validate_input(1000));
        assert!(!validate_input(-1));
        assert!(!validate_input(1001));
    }
 
    // 
    #[test]
    fn test_validated_add_boundary() {
        assert_eq!(validated_add(0, 0), Ok(0));
        assert_eq!(validated_add(1000, 0), Ok(1000));
        assert!(validated_add(1001, 0).is_err());
    }
 
    //  panic
    #[test]
    #[should_panic(expected = "overflow")]
    fn test_overflow_panic() {
        //  panic 
        panic!("overflow: result exceeds maximum");
    }
 
    //  Result ? 
    #[test]
    fn test_with_result() -> Result<(), String> {
        let result = validated_add(10, 20)?;
        assert_eq!(result, 30);
        Ok(())
    }
 
    // 
    #[test]
    #[ignore]
    fn expensive_test() {
        // cargo test -- --ignored 
        std::thread::sleep(std::time::Duration::from_secs(60));
    }
}

#[cfg(test)]
mod tests {
    use super::*;
 
    // Fixture— 
    fn create_test_db() -> Database {
        Database::new(":memory:").expect("failed to create test db")
    }
 
    //  — 
    fn init_test_logging() {
        let _ = tracing_subscriber::fmt()
            .with_test_mode(true)  //  test output
            .try_init();
    }
 
    // 
    fn temp_dir() -> tempfile::TempDir {
        tempfile::tempdir().expect("failed to create temp dir")
    }
 
    #[test]
    fn test_database_operations() {
        init_test_logging();
        let db = create_test_db();
 
        // 
        db.insert("key", "value").unwrap();
        assert_eq!(db.get("key").unwrap(), Some("value".to_string()));
 
        // 
        db.delete("key").unwrap();
        assert_eq!(db.get("key").unwrap(), None);
    }
 
    // 
    #[test]
    fn test_with_message() {
        let data = vec![1, 2, 3];
        assert!(
            data.contains(&2),
            "expected data to contain 2, got: {:?}",
            data
        );
    }
 
    // 
    #[test]
    fn test_float_approx() {
        let result = 0.1 + 0.2;
        assert!(
            (result - 0.3).abs() < f64::EPSILON,
            "floating point mismatch: {} != 0.3",
            result
        );
    }
}

vs

  • src/ #[cfg(test)] mod tests
  • use super::*

  • tests/
  • crate
  • pub API
  • use my_crate::*
my_project/
 src/
    lib.rs              #  lib.rs 
 tests/
    api_test.rs          #  1 crate
    database_test.rs     #  2 crate
    common/
        mod.rs           # 
 Cargo.toml
// tests/api_test.rs —  crate
use my_project::api::Client;  //  pub API
 
#[tokio::test]
async fn test_api_client() {
    let client = Client::new("http://localhost:8080");
    let response = client.get("/health").await.unwrap();
    assert_eq!(response.status(), 200);
}
// tests/common/mod.rs — 
pub fn setup() {
    // 
    tracing_subscriber::fmt()
        .with_test_mode(true)
        .try_init()
        .ok();
}
 
pub fn test_db_url() -> String {
    std::env::var("TEST_DATABASE_URL")
        .unwrap_or_else(|_| "sqlite::memory:".to_string())
}

cargo test

# 
cargo test
 
#  "add" 
cargo test add
 
# 
cargo test --test api_test
 
# 
cargo test tests::test_add
 
# 
cargo test -- --ignored
 
# 
cargo test -- --include-ignored
 
# 
cargo test -- --test-threads=1
 
# 
cargo test -- --nocapture
 
# 
cargo test -- --list

Rust doc comment cargo test

/// 
///
/// # Examples
///
/// ```
/// use my_crate::add;
/// assert_eq!(add(2, 3), 5);
/// ```
///
/// # Panics
///
///  panic
///
/// ```should_panic
/// use my_crate::add;
/// add(-1, 0);
/// ```
///
///  Result 
///
/// ```
/// # fn main() -> Result<(), String> {
/// use my_crate::validated_add;
/// let result = validated_add(10, 20)?;
/// assert_eq!(result, 30);
/// # Ok(())
/// # }
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}
 
// 
/// 
///
/// ```
/// # //  # 
/// # fn main() {
/// let mut map = std::collections::HashMap::new();
/// map.insert("key", "value");
/// assert_eq!(map.get("key"), Some(&"value"));
/// # }
/// ```
pub fn example() {}

mockall — Mock

use mockall::automock;
use mockall::predicate::*;
 
//  trait
#[automock]  //  MockTraitName
trait UserRepository {
    fn find_by_id(&self, id: u64) -> Result<User, Error>;
    fn save(&self, user: &User) -> Result<(), Error>;
    fn count(&self) -> Result<usize, Error>;
}
 
//  trait
struct UserService {
    repo: Box<dyn UserRepository>,
}
 
impl UserService {
    fn new(repo: Box<dyn UserRepository>) -> Self {
        Self { repo }
    }
 
    fn get_user(&self, id: u64) -> Result<User, Error> {
        self.repo.find_by_id(id)
    }
 
    fn create_user(&self, name: &str) -> Result<User, Error> {
        let user = User { id: 0, name: name.to_string() };
        self.repo.save(&user)?;
        Ok(user)
    }
}
 
#[cfg(test)]
mod tests {
    use super::*;
    use mockall::predicate;
 
    #[test]
    fn test_get_user() {
        let mut mock_repo = MockUserRepository::new();
 
        // 
        mock_repo
            .expect_find_by_id()
            .with(predicate::eq(42))           // 
            .times(1)                           // 
            .returning(|id| {
                Ok(User { id, name: "Alice".into() })
            });
 
        let service = UserService::new(Box::new(mock_repo));
        let user = service.get_user(42).unwrap();
        assert_eq!(user.name, "Alice");
    }
 
    #[test]
    fn test_create_user() {
        let mut mock_repo = MockUserRepository::new();
 
        mock_repo
            .expect_save()
            .times(1)
            .returning(|_| Ok(()));
 
        let service = UserService::new(Box::new(mock_repo));
        let user = service.create_user("Bob").unwrap();
        assert_eq!(user.name, "Bob");
    }
 
    #[test]
    fn test_user_not_found() {
        let mut mock_repo = MockUserRepository::new();
 
        mock_repo
            .expect_find_by_id()
            .returning(|_| Err(Error::NotFound));
 
        let service = UserService::new(Box::new(mock_repo));
        assert!(service.get_user(999).is_err());
    }
}

proptest —

#[cfg(test)]
mod tests {
    use proptest::prelude::*;
 
    //  Arbitrary
    #[derive(Debug, Clone)]
    struct Point {
        x: f64,
        y: f64,
    }
 
    impl Point {
        fn distance_to_origin(&self) -> f64 {
            (self.x * self.x + self.y * self.y).sqrt()
        }
    }
 
    //  Arbitrary 
    impl Arbitrary for Point {
        type Parameters = ();
        type Strategy = BoxedStrategy<Self>;
 
        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
            (-1000.0f64..1000.0)
                .prop_flat_map(|x| {
                    (-1000.0f64..1000.0).prop_map(move |y| Point { x, y })
                })
                .boxed()
        }
    }
 
    proptest! {
        //  1: 
        #[test]
        fn test_add_commutative(a: i32, b: i32) {
            prop_assert_eq!(add(a, b), add(b, a));
        }
 
        //  2: 
        #[test]
        fn test_add_associative(a: i32, b: i32, c: i32) {
            prop_assert_eq!(add(add(a, b), c), add(a, add(b, c)));
        }
 
        //  3: 
        #[test]
        fn test_add_identity(a: i32) {
            prop_assert_eq!(add(a, 0), a);
        }
 
        //  4: 
        #[test]
        fn test_sort_ordered(mut v: Vec<i32>) {
            v.sort();
            for w in v.windows(2) {
                prop_assert!(w[0] <= w[1]);
            }
        }
 
        //  5: 
        #[test]
        fn test_distance_non_negative(point in any::<Point>()) {
            prop_assert!(point.distance_to_origin() >= 0.0);
        }
 
        //  6: 
        #[test]
        fn test_encode_decode_roundtrip(s in ".*") {
            let encoded = encode(&s);
            let decoded = decode(&encoded).unwrap();
            prop_assert_eq!(s, decoded);
        }
    }
}

**proptest Shrinking **

proptest ""

proptest! {
    #[test]
    fn test_no_negative(v: Vec<i32>) {
        //  v 
        for &x in &v {
            prop_assert!(x >= 0, "found negative: {}", x);
        }
    }
}
// proptest :
// FAILED: Shrinking found: [-1]  ← 
//  Vec

criterion —

// benches/my_benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
 
fn fibonacci(n: u64) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}
 
fn criterion_benchmark(c: &mut Criterion) {
    // 
    c.bench_function("fib 20", |b| {
        b.iter(|| fibonacci(black_box(20)))
    });
 
    //  — 
    let mut group = c.benchmark_group("fibonacci");
    for i in [10, 20, 30] {
        group.bench_with_input(format!("fib_{}", i), &i, |b, &i| {
            b.iter(|| fibonacci(black_box(i)));
        });
    }
    group.finish();
 
    // 
    c.bench_function("vector allocation", |b| {
        b.iter(|| {
            let v: Vec<i32> = (0..1000).collect();
            black_box(v);
        })
    });
}
 
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
# 
cargo bench
 
# 
cargo bench -- --save-baseline before
cargo bench -- --baseline before
 
#  HTML 
# ( Cargo.toml  criterion features = ["html_reports"])

# 
cargo install cargo-tarpaulin
 
# 
cargo tarpaulin --all-features
 
# 
cargo tarpaulin -p my-crate
 
# 
cargo tarpaulin --exclude my_crate::tests --exclude my_crate::benches
 
# 
cargo tarpaulin --out Html      # HTML 
cargo tarpaulin --out Xml       # Cobertura XMLCI 
cargo tarpaulin --out Json       # JSON 
 
# CI 
cargo tarpaulin --fail-under 80

>80%
>90%
>70%
>80%

CI/CD

# .github/workflows/ci.yml
name: CI
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
env:
  CARGO_TERM_COLOR: always
  RUSTFLAGS: -Dwarnings
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy
 
      - name: Cache cargo registry
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
 
      - name: Check formatting
        run: cargo fmt --all -- --check
 
      - name: Run clippy
        run: cargo clippy --all-targets --all-features
 
      - name: Run tests
        run: cargo test --all-features
 
      - name: Security audit
        run: cargo install cargo-audit && cargo audit
 
      - name: Code coverage
        run: |
          cargo install cargo-tarpaulin
          cargo tarpaulin --all-features --fail-under 80 --out Xml
 
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          file: cobertura.xml

**CI **

  1. ****~/.cargo/registry target/
  2. ****clippyfmttest
  3. **** cargo nextest cargo test
  4. **** PR