Testing Guidelines
This page covers testing conventions and practices for cascette-rs.
Test Organization
Module Structure
Tests live in the same file as the code they test, using a #[cfg(test)] module:
#![allow(unused)]
fn main() {
pub fn parse_header(data: &[u8]) -> Result<Header, ParseError> {
// Implementation
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_header_with_valid_data_returns_header() {
// Test implementation
}
}
}
Nested Modules for Large Files
For files with many tests, use nested modules to group related tests:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
mod parsing {
use super::*;
#[test]
fn test_parse_entry_from_valid_bytes() { ... }
#[test]
fn test_parse_entry_from_truncated_bytes_returns_error() { ... }
}
mod building {
use super::*;
#[test]
fn test_builder_with_entries_produces_sorted_output() { ... }
}
mod edge_cases {
use super::*;
#[test]
fn test_edge_empty_input_returns_empty_result() { ... }
}
}
}
Test Naming Convention
Pattern
Use this naming pattern for test functions:
test_<subject>_<condition>_<expected_outcome>
Components:
| Part | Description | Example |
|---|---|---|
subject | What is being tested | parser, builder, entry |
condition | The scenario or input | with_valid_data, from_empty_input |
expected_outcome | What should happen | returns_struct, returns_error |
Examples
Parsing tests:
#![allow(unused)]
fn main() {
// Good - specific and descriptive
fn test_parse_header_with_valid_magic_returns_header() { ... }
fn test_parse_header_with_invalid_magic_returns_error() { ... }
fn test_parse_entry_from_truncated_data_returns_incomplete_error() { ... }
// Bad - too vague
fn test_parse() { ... }
fn test_header() { ... }
fn test_error() { ... }
}
Building tests:
#![allow(unused)]
fn main() {
// Good
fn test_builder_with_single_entry_creates_valid_output() { ... }
fn test_builder_with_unsorted_entries_sorts_before_writing() { ... }
// Bad
fn test_builder() { ... }
fn test_build() { ... }
}
Round-trip tests:
#![allow(unused)]
fn main() {
// Good - suffix with _round_trip
fn test_index_entry_round_trip_preserves_all_fields() { ... }
fn test_blte_compression_round_trip_matches_original() { ... }
// Bad
fn test_round_trip() { ... } // Round trip of what?
}
Category Prefixes
Use consistent prefixes for special test categories:
| Prefix | Use Case | Example |
|---|---|---|
test_edge_* | Edge cases and boundary conditions | test_edge_empty_input_handled |
test_error_* | Error path validation | test_error_invalid_checksum_detected |
*_round_trip | Serialization/deserialization | test_config_round_trip |
Edge case examples:
#![allow(unused)]
fn main() {
fn test_edge_empty_index_builds_successfully() { ... }
fn test_edge_single_entry_is_searchable() { ... }
fn test_edge_max_u32_offset_handled() { ... }
fn test_edge_zero_length_data_returns_empty() { ... }
}
Error handling examples:
#![allow(unused)]
fn main() {
fn test_error_truncated_footer_returns_parse_error() { ... }
fn test_error_invalid_checksum_returns_mismatch() { ... }
fn test_error_unsorted_entries_rejected() { ... }
}
Test Types
Unit Tests
Test individual functions in isolation:
#![allow(unused)]
fn main() {
#[test]
fn test_jenkins96_hash_with_known_input_produces_expected_output() {
let result = Jenkins96::hash(b"test");
assert_eq!(result.hash32, 0x12345678); // Known value
}
}
Integration Tests
Place in tests/ directory for testing public APIs:
crates/cascette-formats/
├── src/
│ └── lib.rs
└── tests/
└── archive_integration.rs
Property-Based Tests
Use proptest for testing invariants across many inputs:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod proptest_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn round_trip_preserves_entries(entries in prop::collection::vec(any::<Entry>(), 0..100)) {
let built = build(&entries);
let parsed = parse(&built)?;
prop_assert_eq!(entries, parsed);
}
}
}
}
Property test naming (inside proptest! macro):
- No
test_prefix needed (macro adds it) - Describe the property being verified
- Examples:
round_trip_preserves_entries,checksum_detects_corruption
Assertions
Use pretty_assertions
Import pretty_assertions for better diff output on failures:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
#[test]
fn test_something() {
assert_eq!(expected, actual); // Shows colored diff on failure
}
}
}
Common Assertions
| Assertion | Use Case |
|---|---|
assert_eq!(expected, actual) | Value equality |
assert_ne!(a, b) | Values differ |
assert!(condition) | Boolean conditions |
assert!(result.is_ok()) | Success check |
assert!(result.is_err()) | Error check |
matches!(value, pattern) | Pattern matching |
Error Assertions
Test specific error types:
#![allow(unused)]
fn main() {
#[test]
fn test_parse_with_invalid_data_returns_checksum_error() {
let result = parse(invalid_data);
assert!(matches!(
result,
Err(ParseError::ChecksumMismatch { .. })
));
}
}
Running Tests
This project uses cargo-nextest for faster, parallel test execution with better output formatting.
Basic Commands
# Run all tests with nextest (recommended)
cargo nextest run --workspace
# Run tests with CI profile (stricter timeouts, immediate output on failures)
cargo nextest run --profile ci --workspace
# Run tests for a specific crate
cargo nextest run -p cascette-formats
cargo nextest run --profile ci -p cascette-formats
# Run tests matching a pattern
cargo nextest run --workspace edge_ # All edge case tests
cargo nextest run --workspace error_ # All error tests
cargo nextest run --workspace round_trip # All round-trip tests
# Run a specific test
cargo nextest run -p cascette-formats test_parse_header_with_valid_data
Feature Combinations
Test with different feature combinations:
# Default features
cargo test --workspace
# No default features (minimal build)
cargo test --workspace --no-default-features
# All features
cargo test --workspace --all-features
Code Coverage
Generate coverage reports:
# Generate LCOV report
cargo llvm-cov --workspace --lcov --output-path lcov.info
# Generate HTML report
cargo llvm-cov --workspace --html
# Open HTML report
open target/llvm-cov/html/index.html
Test Data
Embedded Test Data
For small test cases, embed data directly in tests:
#![allow(unused)]
fn main() {
#[test]
fn test_parse_minimal_header() {
let data = [
0x42, 0x4C, 0x54, 0x45, // Magic: "BLTE"
0x00, 0x00, 0x00, 0x10, // Header size: 16
];
let header = parse_header(&data).expect("should parse");
assert_eq!(header.magic, b"BLTE");
}
}
Test Fixtures
For larger test files, use the include_bytes! macro or test fixtures:
#![allow(unused)]
fn main() {
const TEST_INDEX: &[u8] = include_bytes!("fixtures/sample.index");
#[test]
fn test_parse_real_index_file() {
let index = ArchiveIndex::parse(TEST_INDEX).expect("should parse");
assert!(!index.entries.is_empty());
}
}
Property Test Strategies
Define reusable strategies for property tests:
#![allow(unused)]
fn main() {
fn valid_entry_strategy() -> impl Strategy<Value = IndexEntry> {
(
prop::array::uniform16(any::<u8>()), // 16-byte key
0u32..u32::MAX, // offset
1u32..1_000_000, // size
).prop_map(|(key, offset, size)| {
IndexEntry { key: key.to_vec(), offset, size, archive_index: None }
})
}
}
CI Integration
Tests run automatically on every pull request using cargo-nextest. The CI workflow:
- Runs
cargo nextest run --profile ci --workspacewith default features - Runs tests with
--no-default-featureson changed crates - Tests each changed crate individually on stable Rust
- Collects code coverage using
cargo llvm-cov --nextestand uploads to Codecov
See .github/workflows/ci.yml for the full configuration.
Nextest Profiles
The project uses three nextest profiles configured in .config/nextest.toml:
| Profile | Description | Use Case |
|---|---|---|
default | Standard timeouts, final output on completion | Local development |
ci | Stricter timeouts, immediate output on failures | CI, PR checks |
release | Release build with optimizations | Performance testing |
Cargo Aliases
Convenient cargo aliases are defined in .cargo/config.toml:
cargo nextest-all # All tests with default profile
cargo nextest-lib # Library tests only
cargo nextest-ci # All tests with CI profile
cargo nextest-release # All tests with release profile
cargo nextest-unit # Unit tests only
cargo nextest-integration # Integration tests only
Performance Profiling
Flamegraphs
The project supports flamegraph generation using cargo-flamegraph. Flamegraphs help visualize CPU time spent in different functions during execution.
Generating Flamegraphs Locally
# Generate flamegraph for benchmarks
cargo flamegraph --bench throughput -- --bench
# Generate flamegraph for a binary
cargo flamegraph --bin cascette-ribbit -- --help
# Generate flamegraph for tests
cargo flamegraph --test integration
# Specify output location (flamegraph.svg is created in working directory by default)
cargo flamegraph --output target/flamegraphs/flamegraph.svg --bench throughput -- --bench
Flamegraph outputs are stored in target/flamegraphs/ and ignored by git.
CI Flamegraph Generation
The .github/workflows/profiling.yml workflow generates flamegraphs automatically:
- Trigger: Manual via
workflow_dispatchor commits with[perf]in the message - Targets:
bench(default),test,binary - Output: Uploaded as artifacts and posted to PR comments
To trigger a flamegraph run:
git commit -m "Add performance optimization [perf]"
git push
Or manually trigger via GitHub Actions UI with a target selector.
Benchmarking
The project uses criterion for benchmarking.
# Run all benchmarks
cargo bench
# Run specific benchmark
cargo bench --bench throughput
# Generate HTML report
cargo bench --bench throughput -- --output-format html
open target/criterion/report/index.html
Benchmark Regression Detection
The profiling workflow automatically detects performance regressions:
- Runs on main branch pushes
- Uses
benchmark-action/github-action-benchmarkto store results - Alerts when performance degrades by >200%
- Posts comments to commits with regression alerts
Benchmark data is stored in GitHub Actions cache for historical comparison.