Chapter 23: Testing Framework - Comprehensive Testing Patterns
This chapter covers Pierre’s testing infrastructure including database testing, integration patterns, synthetic data generation, async testing, error testing, and test organization best practices.
Database Testing Patterns
Pierre uses in-memory SQLite databases for fast, isolated tests without external dependencies.
In-Memory Database Setup
Source: tests/database_memory_test.rs:18-71
#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_memory_database_no_physical_files() -> Result<()> {
let encryption_key = generate_encryption_key().to_vec();
// Create in-memory database - NO physical files
let database = Database::new("sqlite::memory:", encryption_key).await?;
// Verify no physical files are created
let current_dir = std::env::current_dir()?;
let entries = fs::read_dir(¤t_dir)?;
for entry in entries {
let entry = entry?;
let filename = entry.file_name();
let filename_str = filename.to_string_lossy();
assert!(
!filename_str.starts_with(":memory:test_"),
"Found physical file that should be in-memory: {filename_str}"
);
}
// Test basic database functionality
let user = User::new(
"test@memory.test".to_owned(),
"password_hash".to_owned(),
Some("Memory Test User".to_owned()),
);
let user_id = database.create_user(&user).await?;
let retrieved_user = database.get_user(user_id).await?.unwrap();
assert_eq!(retrieved_user.email, "test@memory.test");
assert_eq!(retrieved_user.display_name, Some("Memory Test User".to_owned()));
Ok(())
}
}
Benefits:
- Fast: No disk I/O, tests run in milliseconds
- Isolated: Each test gets independent database
- No cleanup: Memory automatically freed after test
- Deterministic: No race conditions from shared state
Database Isolation Testing
Source: tests/database_memory_test.rs:74-126
#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_multiple_memory_databases_isolated() -> Result<()> {
let encryption_key1 = generate_encryption_key().to_vec();
let encryption_key2 = generate_encryption_key().to_vec();
// Create two separate in-memory databases
let database1 = Database::new("sqlite::memory:", encryption_key1).await?;
let database2 = Database::new("sqlite::memory:", encryption_key2).await?;
// Create users in each database
let user1 = User::new(
"user1@test.com".to_owned(),
"hash1".to_owned(),
Some("User 1".to_owned()),
);
let user2 = User::new(
"user2@test.com".to_owned(),
"hash2".to_owned(),
Some("User 2".to_owned()),
);
let user1_id = database1.create_user(&user1).await?;
let user2_id = database2.create_user(&user2).await?;
// Verify isolation - each database only contains its own user
assert!(database1.get_user(user1_id).await?.is_some());
assert!(database2.get_user(user2_id).await?.is_some());
// User1 should not exist in database2 and vice versa
assert!(database2.get_user(user1_id).await?.is_none());
assert!(database1.get_user(user2_id).await?.is_none());
Ok(())
}
}
Why isolation matters: Tests can run in parallel without interfering. Each test gets clean database state.
Test Fixture Helpers
Common test fixtures (tests/common.rs - conceptual):
#![allow(unused)]
fn main() {
/// Create test database with migrations applied
pub async fn create_test_database() -> Result<Arc<Database>> {
let encryption_key = generate_encryption_key().to_vec();
let database = Database::new("sqlite::memory:", encryption_key).await?;
database.migrate().await?;
Ok(Arc::new(database))
}
/// Create test auth manager with default config
pub fn create_test_auth_manager() -> Arc<AuthManager> {
Arc::new(AuthManager::new())
}
/// Create test cache
pub async fn create_test_cache() -> Result<Arc<Cache>> {
Ok(Arc::new(Cache::new()))
}
/// Initialize server config from environment
pub fn init_server_config() {
std::env::set_var("JWT_SECRET", "test_jwt_secret");
std::env::set_var("ENCRYPTION_KEY", "test_encryption_key_32_bytes_long");
}
}
Pattern: Centralized test helpers reduce duplication and ensure consistent test setup.
Integration Testing Patterns
Pierre tests MCP protocol handlers using structured JSON-RPC requests.
MCP Request Helpers
Source: tests/mcp_protocol_comprehensive_test.rs:27-47
#![allow(unused)]
fn main() {
/// Test helper to create MCP request
fn create_mcp_request(method: &str, params: Option<&Value>, id: Option<Value>) -> Value {
json!({
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": id.unwrap_or_else(|| json!(1))
})
}
/// Test helper to create authenticated MCP request
fn create_auth_mcp_request(
method: &str,
params: Option<&Value>,
token: &str,
id: Option<Value>,
) -> Value {
let mut request = create_mcp_request(method, params, id);
request["auth_token"] = json!(token);
request
}
}
MCP Protocol Integration Test
Source: tests/mcp_protocol_comprehensive_test.rs:49-77
#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_mcp_initialize_request() -> Result<()> {
common::init_server_config();
let database = common::create_test_database().await?;
let auth_manager = common::create_test_auth_manager();
let config = Arc::new(ServerConfig::from_env()?);
let cache = common::create_test_cache().await.unwrap();
let resources = Arc::new(ServerResources::new(
(*database).clone(),
(*auth_manager).clone(),
TEST_JWT_SECRET,
config,
cache,
2048, // Use 2048-bit RSA keys for faster test execution
Some(common::get_shared_test_jwks()),
));
let server = MultiTenantMcpServer::new(resources);
// Test initialize request
let _request = create_mcp_request("initialize", None, Some(json!("init-1")));
// Validate server is properly initialized
let _ = server.database();
Ok(())
}
}
Pattern: Integration tests validate component interactions (server → database → auth) without mocking.
Authentication Testing
Source: tests/mcp_protocol_comprehensive_test.rs:137-175
#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_mcp_authenticate_request() -> Result<()> {
common::init_server_config();
let database = common::create_test_database().await?;
let auth_manager = common::create_test_auth_manager();
let config = Arc::new(ServerConfig::from_env()?);
let cache = common::create_test_cache().await.unwrap();
let resources = Arc::new(ServerResources::new(
(*database).clone(),
(*auth_manager).clone(),
TEST_JWT_SECRET,
config,
cache,
2048,
Some(common::get_shared_test_jwks()),
));
let _server = MultiTenantMcpServer::new(resources);
// Create test user
let user = User::new(
"mcp_auth@example.com".to_owned(),
"password123".to_owned(),
Some("MCP Auth Test".to_owned()),
);
database.create_user(&user).await?;
// Test authenticate request format
let auth_params = json!({
"email": "mcp_auth@example.com",
"password": "password123"
});
let request = create_mcp_request("authenticate", Some(&auth_params), Some(json!("auth-1")));
assert_eq!(request["method"], "authenticate");
assert_eq!(request["params"]["email"], "mcp_auth@example.com");
Ok(())
}
}
Pattern: Create test user → Construct auth request → Validate request structure.
Async Testing Patterns
Pierre uses #[tokio::test] for async test execution.
Async Test Basics
#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_async_database_operation() -> Result<()> {
let database = create_test_database().await?;
// Async operations work naturally
let user = User::new("test@example.com".to_owned(), "hash".to_owned(), None);
let user_id = database.create_user(&user).await?;
// Multiple awaits in sequence
let retrieved = database.get_user(user_id).await?;
assert!(retrieved.is_some());
Ok(())
}
}
tokio::test features:
- Multi-threaded runtime: Tests run on tokio runtime
- Async/await support: Natural async syntax
- Automatic cleanup: Runtime shut down after test
- Error propagation:
Result<()>with?operator
Concurrent Async Operations
#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_concurrent_database_writes() -> Result<()> {
let database = create_test_database().await?;
// Spawn multiple concurrent tasks
let handles: Vec<_> = (0..10)
.map(|i| {
let db = database.clone();
tokio::spawn(async move {
let user = User::new(
format!("user{}@test.com", i),
"hash".to_owned(),
None,
);
db.create_user(&user).await
})
})
.collect();
// Wait for all tasks to complete
for handle in handles {
handle.await??;
}
Ok(())
}
}
Pattern: Test concurrent behavior with tokio::spawn to validate thread safety.
Synthetic Data Generation
Pierre uses deterministic synthetic data for reproducible tests (covered in Chapter 14).
Key benefits:
- No OAuth required: Tests run without external API dependencies
- Deterministic: Seeded RNG ensures same data every run
- Realistic: Physiologically plausible activity data
- Fast: In-memory synthetic provider, no network calls
Usage example (tests/intelligence_synthetic_helpers_test.rs):
#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_beginner_progression_algorithm() {
let mut builder = SyntheticDataBuilder::new(42); // Deterministic seed
let activities = builder.generate_pattern(TrainingPattern::BeginnerRunnerImproving);
let provider = SyntheticProvider::with_activities(activities);
// Test intelligence algorithms without OAuth
let trends = analyze_performance_trends(&provider).await?;
assert!(trends.pace_improvement > 0.30); // Expect 35% improvement
}
}
Test Helpers and Scenario Builders
Pierre provides reusable test helpers for common testing patterns.
Scenario-Based Testing
Source: tests/helpers/test_utils.rs:9-33
#![allow(unused)]
fn main() {
/// Test scenarios for intelligence testing
#[derive(Debug, Clone, Copy)]
pub enum TestScenario {
/// Beginner runner showing 35% improvement over 6 weeks
BeginnerRunnerImproving,
/// Experienced cyclist with stable, consistent performance
ExperiencedCyclistConsistent,
/// Athlete showing signs of overtraining (TSB < -30)
OvertrainingRisk,
/// Return from injury with gradual progression
InjuryRecovery,
}
impl TestScenario {
/// Get the corresponding pattern from synthetic data builder
#[must_use]
pub const fn to_training_pattern(self) -> TrainingPattern {
match self {
Self::BeginnerRunnerImproving => TrainingPattern::BeginnerRunnerImproving,
Self::ExperiencedCyclistConsistent => TrainingPattern::ExperiencedCyclistConsistent,
Self::OvertrainingRisk => TrainingPattern::Overtraining,
Self::InjuryRecovery => TrainingPattern::InjuryRecovery,
}
}
}
}
Scenario Provider Creation
Source: tests/helpers/test_utils.rs:35-42
#![allow(unused)]
fn main() {
/// Create a synthetic provider with pre-configured scenario data
#[must_use]
pub fn create_synthetic_provider_with_scenario(scenario: TestScenario) -> SyntheticProvider {
let mut builder = SyntheticDataBuilder::new(42); // Deterministic seed
let activities = builder.generate_pattern(scenario.to_training_pattern());
SyntheticProvider::with_activities(activities)
}
}
Usage:
#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_overtraining_detection() -> Result<()> {
let provider = create_synthetic_provider_with_scenario(TestScenario::OvertrainingRisk);
let recovery = calculate_recovery_score(&provider).await?;
assert!(recovery.tsb < -30.0); // Overtraining threshold
Ok(())
}
}
Benefits:
- Readable tests:
TestScenario::BeginnerRunnerImprovingvs raw data construction - Reusable: Same scenarios across multiple test files
- Maintainable: Change scenario in one place, all tests update
Error Testing Patterns
Test error conditions explicitly to validate error handling.
Testing Error Cases
#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_duplicate_user_email_rejected() -> Result<()> {
let database = create_test_database().await?;
let user1 = User::new("duplicate@test.com".to_owned(), "hash1".to_owned(), None);
let user2 = User::new("duplicate@test.com".to_owned(), "hash2".to_owned(), None);
// First user succeeds
database.create_user(&user1).await?;
// Second user with same email fails
let result = database.create_user(&user2).await;
assert!(result.is_err());
// Verify error type
let err = result.unwrap_err();
assert!(err.to_string().contains("UNIQUE constraint"));
Ok(())
}
}
Testing Validation Errors
#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_invalid_email_rejected() -> Result<()> {
use pierre_mcp_server::database_plugins::shared::validation::validate_email;
// Test various invalid email formats
let invalid_emails = vec![
"notanemail",
"@test.com",
"test@",
"a@b",
"",
];
for email in invalid_emails {
let result = validate_email(email);
assert!(result.is_err(), "Email '{}' should be invalid", email);
}
// Valid email passes
assert!(validate_email("valid@example.com").is_ok());
Ok(())
}
}
Pattern: Test both success path AND failure paths to ensure error handling works.
Test Organization
Pierre organizes tests by scope and type with 1,635 lines of test helper code.
Test directory structure:
tests/
├── helpers/ # 1,635 lines of shared test utilities
│ ├── synthetic_data.rs # Deterministic test data generation
│ ├── synthetic_provider.rs # In-memory provider for testing
│ └── test_utils.rs # Scenario builders and assertions
├── database_memory_test.rs # Database isolation tests
├── mcp_protocol_comprehensive_test.rs # MCP integration tests
├── admin_jwt_test.rs # JWT authentication tests
├── oauth_e2e_test.rs # OAuth flow E2E tests
├── intelligence_recovery_calculator_test.rs # Algorithm tests
├── pagination_test.rs # Pagination logic tests
├── configuration_profiles_test.rs # Config validation tests
└── [40+ additional test files]
Test categories:
- Database tests: In-memory isolation, transaction handling, migration validation
- Integration tests: MCP protocol, OAuth flows, provider interactions
- Algorithm tests: Recovery calculations, nutrition calculations, performance analysis
- E2E tests: Full user workflows from authentication to data retrieval
- Unit tests: Validation functions, enum conversions, mappers
Key Test Patterns
Pattern 1: Builder for test data
#![allow(unused)]
fn main() {
let activity = ActivityBuilder::new(SportType::Run)
.distance_km(10.0)
.duration_minutes(50)
.average_hr(150)
.build();
}
Pattern 2: Seeded RNG for determinism
#![allow(unused)]
fn main() {
let mut builder = SyntheticDataBuilder::new(42); // Same seed = same data
}
Pattern 3: Synthetic provider for isolation
#![allow(unused)]
fn main() {
let provider = SyntheticProvider::with_activities(vec![activity1, activity2]);
let result = service.analyze(&provider).await?;
}
Key Takeaways
-
In-memory databases:
sqlite::memory:provides fast, isolated tests without physical files or cleanup overhead. -
Database isolation: Each test gets independent database instance, enabling safe parallel test execution.
-
Test fixtures: Centralized helpers like
create_test_database()ensure consistent test setup across all tests. -
Integration testing: MCP protocol tests validate component interactions (server → database → auth) without mocking.
-
JSON-RPC helpers:
create_mcp_request()andcreate_auth_mcp_request()simplify MCP protocol testing. -
Async testing:
#[tokio::test]provides multi-threaded async runtime for natural async/await syntax in tests. -
Concurrent testing:
tokio::spawnvalidates thread safety by testing concurrent database writes and reads. -
Scenario-based testing:
TestScenarioenum provides readable, reusable test scenarios (BeginnerRunnerImproving, OvertrainingRisk). -
Synthetic data: Deterministic test data with seeded RNG (
SyntheticDataBuilder::new(42)) ensures reproducible tests without OAuth. -
Error testing: Explicitly test failure paths (duplicate emails, invalid data) to validate error handling works.
-
Test organization: 1,635 lines of helper code in
tests/helpers/plus 40+ test files organized by category. -
Builder pattern: Fluent API for constructing test activities and data structures.
-
Validation testing: Test shared validation functions (
validate_email,validate_tenant_ownership) with multiple invalid inputs. -
No external dependencies: Tests run offline using in-memory databases and synthetic providers.
-
Fast execution: In-memory databases + synthetic data = millisecond test times, enabling rapid development feedback.
Next Chapter: Chapter 24: Design System - Learn about Pierre’s design system, templates, frontend architecture, and user experience patterns.