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
-
- ****
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 testsuse super::*
tests/- crate
pubAPIuse 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 -- --listRust 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] ←
// Veccriterion —
// 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 **
- ****
~/.cargo/registrytarget/ - ****clippyfmttest
- ****
cargo nextestcargo test - **** PR