Living documentation for testing patterns, conventions, and best practices.
Unit tests live in a #[cfg(test)] mod tests block within each module.
Pattern: test_{function_name}_{scenario}
#[test]
fn test_parse_hex_color() { ... }
#[test]
fn test_parse_hex_color_lowercase() { ... }
#[test]
fn test_parse_hex_color_invalid_format() { ... }E2E tests: e2e_{feature}_{scenario}
fn e2e_table_simple_2x2() { ... }
fn e2e_table_nested_tables() { ... }Regression tests: regression_{category}_{bug_description}
fn regression_parsing_empty_markup_tag() { ... }
fn regression_layout_wide_char_overflow() { ... }Property tests: prop_{invariant_being_tested}
fn prop_style_combination_is_associative() { ... }
fn prop_color_roundtrip_preserves_values() { ... }Fuzz tests: Located in tests/fuzz_*.rs
fn fuzz_style_parser_no_panic() { ... }
fn fuzz_markup_parser_handles_arbitrary_input() { ... }Required Prefixes:
| Test Type | Required Prefix | File Location |
|---|---|---|
| Unit tests | test_ |
src/*.rs (in mod tests) |
| E2E tests | e2e_ |
tests/e2e_*.rs |
| Property tests | prop_ |
tests/property_tests.rs |
| Fuzz tests | fuzz_ |
tests/fuzz_*.rs |
| Regression tests | regression_ |
tests/regression_tests.rs |
| Conformance tests | test_ or conformance_ |
tests/conformance_*.rs |
Validation Script:
Run this to check naming compliance:
# Check for test functions missing standard prefixes
rg "#\[test\]" -A 1 tests/*.rs | rg "fn [a-z]" | \
rg -v "fn (test_|e2e_|prop_|fuzz_|regression_|conformance_)" | \
rg -v "fn (new|contents|write|flush|len|clear)" && \
echo "Found non-compliant test names!" || \
echo "All test names comply with standards"Common Anti-Patterns:
| Anti-Pattern | Correct Pattern | Example |
|---|---|---|
fn check_something() |
fn test_something() |
test_color_parsing |
fn verify_output() |
fn e2e_output_verification() |
e2e_table_renders_borders |
fn it_should_work() |
fn test_feature_works() |
test_style_parse_bold |
fn test1() |
fn test_feature_scenario() |
test_color_hex_uppercase |
Descriptive Scenarios:
Good test names describe:
- What is being tested (function/feature)
- When or under what conditions
- Expected outcome (for edge cases)
// Good examples:
fn test_style_parse_with_invalid_color_returns_none() { ... }
fn e2e_table_with_unicode_content_preserves_alignment() { ... }
fn regression_markup_empty_tag_no_panic() { ... }
// Poor examples:
fn test_style1() { ... }
fn test_it_works() { ... }
fn check_table() { ... }tests/
├── common/ # Shared test utilities
│ └── mod.rs
├── e2e_*.rs # End-to-end feature tests
├── property_tests.rs # Property-based tests (proptest)
├── fuzz_*.rs # Fuzz tests
├── regression_tests.rs # Bug regression tests
├── thread_safety.rs # Concurrency and thread safety
├── mutex_poison_recovery.rs # Poison handling tests
├── conformance_*.rs # Python Rich compatibility
├── golden_test.rs # Snapshot/golden tests
└── repro_*.rs # Reproduction tests for specific bugs
Each test file should have a module-level doc comment explaining:
- What the tests cover
- How to run them
- Any special requirements
//! End-to-end tests for Table rendering.
//!
//! Run with: RUST_LOG=debug cargo test --test e2e_table -- --nocapture
For capturing Console output:
#[derive(Clone)]
struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
impl SharedBuffer {
fn new() -> Self {
Self(Arc::new(Mutex::new(Vec::new())))
}
fn contents(&self) -> String {
String::from_utf8_lossy(&self.0.lock().unwrap()).to_string()
}
}
impl Write for SharedBuffer {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.lock().unwrap().write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.0.lock().unwrap().flush()
}
}
fn make_test_console(buffer: SharedBuffer) -> Arc<Console> {
Console::builder()
.force_terminal(true)
.markup(false)
.file(Box::new(buffer))
.build()
.shared()
}Use the common logging helpers for detailed test output:
mod common;
use common::{init_test_logging, log_test_context, test_phase};
#[test]
fn my_test() {
init_test_logging();
test_phase!("setup");
// ... test code
log_test_context!("rendered output", &output);
}Use snapshots for:
- Complex visual output (tables, panels, trees)
- ANSI escape sequence verification
- Conformance with Python Rich
Don't use snapshots for:
- Simple assertions (
assert_eq!) - Dynamic or time-dependent output
- Tests that verify behavior, not output
// In tests/golden_test.rs or tests/demo_showcase_snapshots.rs
#[test]
fn snapshot_table_with_borders() {
let output = render_table_with_borders();
insta::assert_snapshot!(output);
}Use proptest for invariant verification. Located in tests/property_tests.rs.
fn rgb_triplet() -> impl Strategy<Value = (u8, u8, u8)> {
(any::<u8>(), any::<u8>(), any::<u8>())
}
fn random_style() -> impl Strategy<Value = Style> {
// ... complex strategy
}proptest! {
#[test]
fn prop_style_parse_never_panics(s in ".*") {
// Should never panic, even on invalid input
let _ = Style::parse(&s);
}
#[test]
fn prop_color_downgrade_preserves_validity(
r in 0u8..=255u8,
g in 0u8..=255u8,
b in 0u8..=255u8
) {
let color = Color::from_rgb(r, g, b);
let downgraded = color.downgrade(ColorSystem::Standard);
// Verify downgraded color is valid
prop_assert!(!downgraded.get_ansi_codes(true).is_empty());
}
}#[test]
fn e2e_feature_scenario() {
init_test_logging();
tracing::info!("Starting E2E test: feature_scenario");
// 1. Setup
test_phase!("setup");
let console = make_test_console(SharedBuffer::new());
// 2. Action
test_phase!("action");
let output = do_something(&console);
tracing::debug!(output = %output, "Action result");
// 3. Verify
test_phase!("verify");
assert!(output.contains("expected"), "Missing expected content");
assert!(!output.contains("error"), "Unexpected error");
tracing::info!("E2E test passed: feature_scenario");
}Group related tests with section comments:
// =============================================================================
// Scenario 1: Basic Usage
// =============================================================================
#[test]
fn e2e_table_simple_2x2() { ... }
#[test]
fn e2e_table_empty() { ... }
// =============================================================================
// Scenario 2: Edge Cases
// =============================================================================
#[test]
fn e2e_table_wide_content() { ... }rich_rust is an output library with minimal external dependencies. Avoid mocks in most cases.
Use real components for:
- Console (with captured output via SharedBuffer)
- Style parsing
- Rendering pipeline
- Color/ANSI code generation
Terminal detection: Use force_terminal(true) to bypass TTY checks.
let console = Console::builder()
.force_terminal(true) // Pretend we have a TTY
.width(80) // Fixed width
.height(24) // Fixed height
.color_system(ColorSystem::TrueColor)
.build();Time-based tests: Mock sleep durations for faster tests.
let options = LiveOptions {
refresh_per_second: 100.0, // Fast refresh for testing
..Default::default()
};| Category | Target | Priority |
|---|---|---|
| Core (Console, Style, Color) | 90%+ | High |
| Renderables | 80%+ | High |
| Sync/Threading | 95%+ | Critical |
| Interactive | 70%+ | Medium |
| Optional features | 75%+ | Medium |
# Install tarpaulin
cargo install cargo-tarpaulin
# Generate coverage report
cargo tarpaulin --out Html --output-dir coverage/
# Coverage for specific files
cargo tarpaulin --files src/console.rs src/style.rsExclude from coverage analysis:
- Generated code
- Debug-only paths (
#[cfg(debug_assertions)]) - Platform-specific code not testable in CI
Before submitting a PR:
-
cargo testpasses all tests -
cargo clippy --all-targets -- -D warningspasses -
cargo fmt --checkpasses - No new
unwrap()in production code (uselock_recoverfor mutexes)
- Unit tests added for new public APIs
- E2E test added for user-facing behavior
- Documentation updated
- Regression test added in
tests/regression_tests.rs - Test named with pattern:
regression_{category}_{bug_description} - Comment documenting what bug it prevents
- Benchmark added or updated
- No regression in existing benchmarks
- Property test verifies correctness
# Run all tests
cargo test
# Run with output (see println!/tracing)
cargo test -- --nocapture
# Run specific test file
cargo test --test e2e_table
# Run tests matching name pattern
cargo test table
# Run tests with detailed logging
RUST_LOG=debug cargo test --test e2e_table -- --nocapture
# Run in parallel (default) or single-threaded
cargo test -- --test-threads=1# Full CI validation
cargo check --all-targets
cargo clippy --all-targets -- -D warnings
cargo test
cargo fmt --checkFor concurrent code, use these patterns from tests/thread_safety.rs:
#[test]
fn test_concurrent_access() {
use std::sync::Arc;
use std::thread;
let shared = Arc::new(MyStruct::new());
let handles: Vec<_> = (0..4)
.map(|i| {
let s = Arc::clone(&shared);
thread::spawn(move || {
s.do_something(i);
})
})
.collect();
for h in handles {
h.join().expect("thread should not panic");
}
}See tests/mutex_poison_recovery.rs for patterns:
#[test]
fn test_recovery_after_poison() {
let mutex = Mutex::new(42);
// Poison the mutex
let _ = panic::catch_unwind(AssertUnwindSafe(|| {
let _guard = mutex.lock().unwrap();
panic!("intentional");
}));
// Verify recovery works
let guard = lock_recover(&mutex);
assert_eq!(*guard, 42);
}- AGENTS.md: Tooling and workflow guidelines
- RICH_SPEC.md: Python Rich behavior specification
- FEATURE_PARITY.md: Feature comparison matrix
Last updated: 2026-01-28 by OrangeFinch (claude-code/opus-4.5)