name: rust-testing description: > Use when writing unit/integration tests, setting up test infrastructure, mocking with wiremock/mockall, or designing test structure in Rust.
Rust Testing Standards
Test Categories
| Type | Location | Dependencies |
|---|---|---|
| Unit tests | #[cfg(test)] module in source file |
None (no external deps) |
| Integration tests | tests/ directory |
May need database/services |
Commands
cargo test # all tests
cargo test --lib # unit tests only
cargo test --test '*' # integration tests only
cargo test -- --nocapture # show stdout
Unit Tests
Place in a #[cfg(test)] module at the bottom of the source file:
impl Order {
pub fn can_cancel(&self) -> bool {
matches!(self.status, OrderStatus::Pending | OrderStatus::Processing)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_can_cancel_pending_order() {
let order = Order { status: OrderStatus::Pending, ..Default::default() };
assert!(order.can_cancel());
}
}
Naming Convention
// Format: test_{feature}_{scenario}_{expected}
fn test_place_order_with_insufficient_balance_returns_error() { }
// Or "should" style
fn should_return_error_when_balance_insufficient() { }
Integration Tests
tests/
├── common/
│ └── mod.rs # shared helpers (setup_test_db, etc.)
├── api/
│ └── orders_test.rs
└── integration/
└── order_flow_test.rs
HTTP Test Example
#[tokio::test]
async fn test_create_order_success() {
let pool = setup_test_db().await;
let server = TestServer::new(create_test_app(pool.clone()).await).unwrap();
let response = server
.post("/orders")
.add_header("Authorization", format!("Bearer {}", api_key))
.json(&json!({ "product_id": "prod-001", "quantity": 2 }))
.await;
response.assert_status(StatusCode::CREATED);
}
Mocking
Trait Abstraction (hand-written mock)
#[async_trait]
pub trait ExternalApiClient: Send + Sync {
async fn submit(&self, req: &SubmitRequest) -> Result<SubmitResponse, Error>;
}
// Test implementation
pub struct MockExternalApiClient {
pub responses: Arc<Mutex<Vec<Result<SubmitResponse, Error>>>>,
}
mockall Crate (recommended alternative)
For most cases, prefer mockall over hand-written mocks — it auto-generates mock structs from traits:
use mockall::automock;
#[automock]
#[async_trait]
pub trait ExternalApiClient: Send + Sync {
async fn submit(&self, req: &SubmitRequest) -> Result<SubmitResponse, Error>;
}
#[tokio::test]
async fn test_submit_delegates_to_client() {
let mut mock = MockExternalApiClient::new();
mock.expect_submit()
.returning(|_| Ok(SubmitResponse { id: "abc-123".into() }));
let service = OrderService::new(Arc::new(mock));
let result = service.process(&request).await.unwrap();
assert_eq!(result.id, "abc-123");
}
wiremock (HTTP-level mocking)
#[tokio::test]
async fn test_external_api_client() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/api/process"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "abc-123"
})))
.mount(&mock_server)
.await;
let client = ExternalApiClient::new(&mock_server.uri());
let result = client.submit(&request).await.unwrap();
assert_eq!(result.id, "abc-123");
}
Key Coverage Targets
- State machine transitions: e.g., Order status flow (Pending -> Confirmed -> Shipped -> Delivered)
- Billing logic: balance deduction, refunds, proration
- Authorization checks: API key validation, role-based access
- Boundary conditions: null/empty values, limits exceeded, concurrency
Property-Based Testing (advanced)
For logic with wide input ranges, consider proptest to auto-generate test cases:
use proptest::prelude::*;
proptest! {
#[test]
fn test_discount_never_exceeds_total(
price in 1u64..10_000,
discount_pct in 0u8..=100,
) {
let discounted = apply_discount(price, discount_pct);
prop_assert!(discounted <= price);
}
}
proptest is especially useful for parsers, serialization round-trips, and numeric invariants.
Anti-Patterns
// WRONG: unwrap without context
let result = operation().unwrap();
// RIGHT: use expect
let result = operation().expect("operation should succeed with valid input");
// WRONG: hard-coded sleep
std::thread::sleep(Duration::from_secs(1));
// RIGHT: conditional wait with timeout
tokio::time::timeout(Duration::from_secs(5), wait_for_condition()).await;
// WRONG: tests depend on execution order
// RIGHT: each test is fully independent
Checklist
- Unit tests live in
#[cfg(test)]modules - Integration tests live in
tests/directory - External dependencies are mocked (mockall or hand-written traits)
- Test database is cleaned between runs
- Async tests use
#[tokio::test] - Tests are independent of each other