Testing Guide
ClawDesk maintains 610 passing tests across the workspace. This guide covers the test pyramid, conventions, and how to contribute well-tested code.
Test Pyramid
╱╲
╱ E2E ╲ ~30 tests — Full gateway integration
╱────────╲
╱Integration╲ ~180 tests — Cross-crate interactions
╱──────────────╲
╱ Unit Tests ╲ ~400 tests — Single module isolation
╱────────────────────╲
| Level | Location | Scope | Speed |
|---|---|---|---|
| Unit | src/*.rs (inline #[cfg(test)]) | Single function/module | <1ms |
| Integration | tests/*.rs | Cross-module, crate-level | <100ms |
| E2E | tests/e2e/ in gateway | Full HTTP request cycle | <5s |
Running Tests
# All tests (610 total)
cargo test --workspace
# Single crate
cargo test -p clawdesk-domain
# Single test by name
cargo test -p clawdesk-agents test_pipeline_context_assembly
# With output
cargo test --workspace -- --nocapture
# Only unit tests (exclude integration)
cargo test --workspace --lib
# Only integration tests
cargo test --workspace --test '*'
Filtering
# Run all tests matching a pattern
cargo test --workspace fallback
# Run tests in a specific module
cargo test -p clawdesk-domain routing::tests
Writing Unit Tests
Place unit tests in the same file as the code under test:
pub fn calculate_token_budget(max_tokens: usize, reserved: usize) -> usize {
max_tokens.saturating_sub(reserved)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn token_budget_normal() {
assert_eq!(calculate_token_budget(4096, 512), 3584);
}
#[test]
fn token_budget_overflow_protection() {
assert_eq!(calculate_token_budget(100, 200), 0);
}
}
Async Tests
Use #[tokio::test] for async tests:
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn session_store_roundtrip() {
let store = InMemorySessionStore::new();
let session = Session::new("test-channel");
store.save(&session).await.unwrap();
let loaded = store.get(&session.id).await.unwrap();
assert_eq!(loaded.unwrap().id, session.id);
}
}
Writing Integration Tests
Place cross-module tests in tests/ at the crate root:
// tests/pipeline_integration.rs
use clawdesk_agents::{Pipeline, PipelineConfig};
use clawdesk_types::{Message, Session};
#[tokio::test]
async fn full_pipeline_processes_message() {
let config = PipelineConfig::default();
let pipeline = Pipeline::new(config);
let session = Session::new("test");
let message = Message::user("Hello, world!");
let result = pipeline.process(&session, &message).await;
assert!(result.is_ok());
assert!(!result.unwrap().content.is_empty());
}
Mocking
Use trait-based mocking for dependencies. Create mock implementations in test modules:
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
struct MockSessionStore {
sessions: std::sync::Mutex<HashMap<SessionId, Session>>,
}
#[async_trait]
impl SessionStore for MockSessionStore {
async fn get(&self, id: &SessionId) -> Result<Option<Session>, StorageError> {
Ok(self.sessions.lock().unwrap().get(id).cloned())
}
async fn save(&self, session: &Session) -> Result<(), StorageError> {
self.sessions.lock().unwrap().insert(session.id.clone(), session.clone());
Ok(())
}
}
#[tokio::test]
async fn agent_uses_session_store() {
let store = Arc::new(MockSessionStore {
sessions: std::sync::Mutex::new(HashMap::new()),
});
// ... test with mock
}
}
Prefer creating simple mock structs over using mocking frameworks. ClawDesk's trait-based architecture makes manual mocking straightforward.
Test Fixtures
For reusable test data, create helper functions:
#[cfg(test)]
pub(crate) mod fixtures {
use clawdesk_types::*;
pub fn sample_message() -> Message {
Message {
id: MessageId::new(),
role: Role::User,
content: "Test message".into(),
timestamp: chrono::Utc::now(),
..Default::default()
}
}
pub fn sample_session() -> Session {
Session {
id: SessionId::new(),
channel_id: ChannelId::from("test-channel"),
..Default::default()
}
}
}
E2E Tests
End-to-end tests spin up a real gateway and make HTTP requests:
// crates/clawdesk-gateway/tests/e2e/health.rs
use reqwest::StatusCode;
#[tokio::test]
async fn health_endpoint_returns_ok() {
let addr = spawn_test_server().await;
let resp = reqwest::get(format!("http://{addr}/api/v1/health"))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
Coverage
Generate coverage reports with cargo-llvm-cov:
# Install
cargo install cargo-llvm-cov
# Generate HTML report
cargo llvm-cov --workspace --html
# Open the report
open target/llvm-cov/html/index.html
There is no hard coverage threshold, but aim for >80% on domain and agent crates. Infrastructure adapters may have lower coverage when they depend on external services.
Test Conventions
- Test names describe the scenario:
test_fallback_triggers_on_rate_limit - One assertion per logical concept — multiple
assert!calls are fine if testing one behavior - No sleeping — use
tokio::time::pause()for time-dependent tests - Deterministic — tests must not depend on ordering or external state
- Fast — individual tests should complete in under 100ms
CI Integration
All tests run in CI on every pull request:
- cargo fmt --check
- cargo clippy --workspace -- -D warnings
- cargo test --workspace
A PR cannot be merged unless all 610+ tests pass.