| name | quality-unit-testing |
| description | Write high-quality Rust unit tests following best practices. Use when writing new tests, reviewing test code, or improving test quality. Emphasizes clear naming, AAA pattern, isolation, and deployment confidence. |
Quality Unit Testing for Rust
Expert guidance for writing unit tests that catch real bugs and provide deployment confidence.
Core Philosophy
Quality over coverage: Tests should catch real bugs and enable fearless deployment, not just boost coverage percentages.
When to Use This Skill
Use for:
- Writing new unit tests
- Reviewing test code quality
- Improving existing test suites
- Establishing testing standards
- Evaluating test effectiveness
Quick Reference
Test Naming: test_<function>_<scenario>_<expected_behavior>
Use descriptive three-part names for most tests. Well-named two-part names are acceptable for very simple tests:
Preferred (three-part):
#[test]
fn test_process_payment_insufficient_funds_returns_error()
#[test]
fn test_calculate_discount_new_customer_returns_zero()
#[tokio::test]
async fn test_withdraw_valid_amount_decreases_balance()
Acceptable for simple tests (two-part):
#[test]
fn test_new_account_initializes_fields() // When behavior is obvious
#[test]
fn test_default_context_values() // When scenario is implicit
AAA Pattern: Arrange-Act-Assert
Structure tests with clear sections. AAA comments are recommended for complex tests (>10 lines, multiple setup steps, async operations), optional for simple tests:
Complex test (AAA comments recommended):
#[test]
fn test_account_withdraw_valid_amount_decreases_balance() {
// Arrange - Set up test context
let mut account = Account::new(100);
// Act - Execute behavior
let result = account.withdraw(30);
// Assert - Verify outcome
assert!(result.is_ok());
assert_eq!(account.balance(), 70);
}
Simple test (AAA comments optional):
#[test]
fn test_new_account_starts_with_zero_balance() {
let account = Account::new(0);
assert_eq!(account.balance(), 0);
}
Isolation: Mock External Dependencies
- ✅ Mock: APIs, databases, file systems, time/date, external services
- ❌ Don't mock: Value types, pure functions, the code under test
Single Responsibility
Each test verifies ONE behavior with ONE reason to fail.
Speed Target
Milliseconds per test. Unit tests should run instantly.
Detailed Guidance
For detailed information on specific topics, see:
- Test Naming Patterns:
reference/naming-conventions.md - AAA Structure Details:
reference/aaa-pattern.md - Async Testing:
reference/async-testing.md - Test Builders:
reference/test-builders.md - Anti-Patterns to Avoid:
reference/anti-patterns.md
Test Quality Analysis
Installation
The analysis script requires Python 3.8+ and optionally the tomli library for TOML configuration support:
# Install dependencies (optional but recommended)
pip install -r scripts/requirements.txt
# Or install manually
pip install tomli # Only needed for Python < 3.11
Note: Python 3.11+ includes tomllib in the standard library, so no dependencies are needed. For earlier versions, install tomli to use TOML configuration files.
Usage
To analyze test file quality:
# Basic analysis
python scripts/analyze-test-quality.py memory-core/src/lib.rs
# Lenient mode (recommended for existing codebases)
python scripts/analyze-test-quality.py memory-core/src/lib.rs --lenient
# With specific options
python scripts/analyze-test-quality.py memory-core/src/lib.rs \
--allow-two-part-names \
--max-assertions 7
# Using config file (requires tomli for Python < 3.11)
python scripts/analyze-test-quality.py memory-core/src/lib.rs \
--config=.test-quality.toml
Configuration Options
Create .test-quality.toml in your project root:
# Test quality analysis configuration
[naming]
require_three_parts = false # Allow two-part names for simple tests
min_parts = 2
[structure]
require_aaa_comments = "complex" # "always", "complex", or "never"
complex_test_threshold = 10 # Lines count for "complex"
[focus]
max_assertions = 7 # Warning threshold
max_assertions_error = 10 # Error threshold
[severity]
naming_violation = "medium" # "high", "medium", or "low"
missing_aaa_simple = "low"
missing_aaa_complex = "medium"
Templates
Use pre-built templates for consistent test structure:
- Basic unit test:
templates/unit-test.md - Async test:
templates/async-test.md - Test builder pattern:
templates/test-builder.md
Quality Checklists
Before committing tests, verify against:
- Pre-commit checklist:
checklists/pre-commit.md - Code review checklist:
checklists/review.md
Key Success Metrics
You're succeeding when:
- ✅ You deploy without manual testing
- ✅ Test failures pinpoint exact problems
- ✅ Refactoring doesn't break unrelated tests
- ✅ Tests run in milliseconds
- ✅ Every failure is actionable
You need improvement when:
- ❌ Tests are skipped because they're slow
- ❌ Test failures require investigation
- ❌ High coverage but low deployment confidence
- ❌ Flaky tests train team to ignore failures
Rust-Specific Best Practices
1. Use #[tokio::test] for Async Tests
#[tokio::test]
async fn test_async_operation() {
let result = async_function().await;
assert!(result.is_ok());
}
2. Use Result<()> for Tests with Error Handling
#[test]
fn test_operation() -> anyhow::Result<()> {
let result = fallible_operation()?;
assert_eq!(result, expected);
Ok(())
}
3. Use Test Builders for Complex Setup
let episode = TestEpisodeBuilder::new()
.with_task("Test task")
.with_context(context)
.completed(true)
.build();
4. Clean Up with RAII (Drop)
struct TestDb(TempDir);
impl Drop for TestDb {
fn drop(&mut self) {
// Cleanup happens automatically
}
}
5. Use tempfile for File System Tests
#[test]
fn test_file_operation() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test.db");
// Test with path
// Cleanup happens automatically when dir is dropped
}
Project-Specific Patterns
Testing Async Code with Tokio
#[tokio::test]
async fn test_start_episode_valid_task_creates_episode() {
// Arrange
let memory = create_test_memory().await;
let context = TaskContext::default();
// Act
let episode_id = memory.start_episode("Test task", context).await?;
// Assert
assert!(!episode_id.is_empty());
let episode = memory.get_episode(&episode_id).await?;
assert_eq!(episode.task_description, "Test task");
}
Testing Error Cases
#[tokio::test]
async fn test_get_episode_invalid_id_returns_error() {
// Arrange
let memory = create_test_memory().await;
// Act
let result = memory.get_episode("invalid_id").await;
// Assert
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::EpisodeNotFound(_)));
}
Using Test Utilities
use test_utils::*;
#[test]
fn test_episode_completion() {
// Arrange
let episode = create_completed_episode("Test task", true);
// Act & Assert
assert!(episode.is_complete());
assert_eq!(episode.verdict, Some(Verdict::Success));
}
Workflow for Creating Tests
- Understand the code: What behavior needs verification?
- Identify risks: What could break in production?
- Write failing test first (red-green-refactor)
- Apply AAA pattern with clear naming
- Isolate dependencies with proper mocking
- Verify test speed (should be milliseconds)
- Check quality against checklists
- Ensure value: Does this catch real bugs?
Workflow for Reviewing Tests
- Run analysis script:
scripts/analyze-test-quality.py - Check against review checklist:
checklists/review.md - Verify naming follows conventions
- Ensure proper isolation
- Confirm single responsibility
- Check for anti-patterns
- Validate test value
Remember
The goal is deployment confidence, not coverage theater.
Focus testing effort where failures hurt most:
- High Priority: Business logic, data integrity, async correctness
- Medium Priority: Integration points, error handling
- Low Priority: Simple getters/setters, trivial code
Integration with Other Skills
- Use test-runner to execute tests:
cargo test - Use test-fix when tests fail and need diagnosis
- Use code-quality to ensure test code meets standards
- Use rust-code-quality for comprehensive review