Workflow: Testing ​
Updated Mar 2026Overview ​
Rust has first-class testing support built into the language and Cargo. This workflow covers unit tests, integration tests, doc tests, property-based testing, and CI integration.
Testing Strategy ​
Unit Tests ​
rust
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
fn internal_helper(x: i32) -> i32 {
x * 2
}
#[cfg(test)] // Only compiled when running tests
mod tests {
use super::*; // Import parent module
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-1, 1), 0);
}
// Can test private functions!
#[test]
fn test_internal_helper() {
assert_eq!(internal_helper(5), 10);
}
#[test]
#[should_panic(expected = "overflow")]
fn test_overflow() {
add(i32::MAX, 1); // Should panic
}
#[test]
fn test_result() -> Result<(), String> {
let result = add(2, 2);
if result == 4 {
Ok(())
} else {
Err(format!("Expected 4, got {result}"))
}
}
#[test]
#[ignore] // Skip unless explicitly requested
fn expensive_test() {
// Long-running test
std::thread::sleep(std::time::Duration::from_secs(10));
}
}Integration Tests ​
my_project/
├── src/
│ └── lib.rs
└── tests/
├── common/
│ └── mod.rs # Shared test helpers
├── api_tests.rs # Each file is a separate test crate
└── db_tests.rsrust
// tests/common/mod.rs
pub fn setup() -> TestContext {
// Shared setup logic
TestContext::new()
}
// tests/api_tests.rs
mod common;
use my_project::*;
#[test]
fn test_full_workflow() {
let ctx = common::setup();
let result = process_data("input");
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 5);
}Doc Tests ​
rust
/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// use my_crate::add;
///
/// let result = add(2, 3);
/// assert_eq!(result, 5);
/// ```
///
/// Negative numbers work too:
///
/// ```
/// use my_crate::add;
/// assert_eq!(add(-1, 1), 0);
/// ```
///
/// # Panics
///
/// This example shows what NOT to do:
///
/// ```rust,should_panic
/// // This will panic on overflow in debug mode
/// my_crate::add(i32::MAX, 1);
/// ```
///
/// Code that shouldn't be run:
///
/// ```rust,no_run
/// // This connects to a real server
/// my_crate::connect("production.example.com");
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}Test Organization Patterns ​
Running Tests ​
bash
# Run all tests
cargo test
# Run specific test by name
cargo test test_add
# Run tests in specific module
cargo test tests::test_add
# Run specific integration test file
cargo test --test api_tests
# Run only ignored tests
cargo test -- --ignored
# Run all tests including ignored
cargo test -- --include-ignored
# Show println! output (normally captured)
cargo test -- --nocapture
# Run tests serially (default is parallel)
cargo test -- --test-threads=1
# Run only doc tests
cargo test --doc
# Run tests in specific package (workspace)
cargo test -p my-core
# Run tests with specific features
cargo test --features "serde"Assertion Macros ​
rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn assertion_examples() {
// Basic assertions
assert!(true);
assert_eq!(1 + 1, 2);
assert_ne!(1, 2);
// With custom messages
let x = 5;
assert!(x > 0, "x should be positive, got {x}");
assert_eq!(x, 5, "x should be 5 but was {x}");
// Float comparison (no assert_eq for floats!)
let f = 0.1 + 0.2;
assert!((f - 0.3).abs() < f64::EPSILON);
}
}Test Fixtures and Setup ​
rust
struct TestDb {
connection: Connection,
}
impl TestDb {
fn new() -> Self {
let conn = Connection::open_in_memory().unwrap();
conn.execute("CREATE TABLE users (id INTEGER, name TEXT)", []).unwrap();
TestDb { connection: conn }
}
}
impl Drop for TestDb {
fn drop(&mut self) {
// Cleanup happens automatically
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_with_db() {
let db = TestDb::new(); // Setup
db.connection.execute("INSERT INTO users VALUES (1, 'Alice')", []).unwrap();
let count: i32 = db.connection
.query_row("SELECT COUNT(*) FROM users", [], |row| row.get(0))
.unwrap();
assert_eq!(count, 1);
} // TestDb::drop() cleans up
}Property-Based Testing ​
rust
// Cargo.toml: proptest = "1"
use proptest::prelude::*;
fn reverse(s: &str) -> String {
s.chars().rev().collect()
}
proptest! {
#[test]
fn reverse_twice_is_identity(s in "\\PC*") {
assert_eq!(reverse(&reverse(&s)), s);
}
#[test]
fn reverse_preserves_length(s in "\\PC*") {
assert_eq!(reverse(&s).len(), s.len());
}
#[test]
fn add_is_commutative(a in -1000i32..1000, b in -1000i32..1000) {
assert_eq!(add(a, b), add(b, a));
}
}CI Integration ​
yaml
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
- name: Check formatting
run: cargo fmt --check
- name: Clippy
run: cargo clippy -- -D warnings
- name: Build
run: cargo build --verbose
- name: Test
run: cargo test --verbose
- name: Test (all features)
run: cargo test --all-featuresChecklist ​
- [ ] Unit tests for all core logic (in same file with
#[cfg(test)]) - [ ] Integration tests for public API (in
tests/directory) - [ ] Doc tests for all public functions with examples
- [ ] Edge cases: empty input, max values, error paths
- [ ]
cargo testpasses with no failures - [ ]
cargo clippypasses with no warnings - [ ] CI pipeline runs tests on every push