--- description: This is designed to help understand how to do testing in this project. globs: alwaysApply: false --- # Testing Rules and Best Practices ## Testing Organization Best Practices ### Recommended Mocking Libraries For consistent mocking across the codebase, use these libraries: | Library | Purpose | Use Case | |---------|---------|----------| | **mockito** | HTTP service mocking | Mocking external API calls | | **mockall** | Trait/struct mocking | Mocking database and service interfaces | | **mock-it** | Simple mocking | Quick mocks for simple interfaces | | **wiremock** | Advanced HTTP mocking | Complex API scenarios with extended matching | Always prefer dependency injection patterns to enable easy mocking of dependencies. ### Library Code Testing Structure - **Unit Tests**: Include unit tests inside library source files using `#[cfg(test)]` modules - **Integration Tests**: Place integration tests in the lib's `tests/` directory - **Test Utils**: Create a private `test_utils.rs` module in each lib for test utilities specific to that lib - **Scope Isolation**: Test utilities should only be shared within their scope (e.g., libs/database, libs/agents) - **Conditional Testing**: Use conditional compilation or runtime checks to make tests non-blocking (e.g., skip tests if a required API key is missing) ### API (src) Testing Structure - Maintain API-specific tests in the `/api/tests/` directory - Organize integration tests to mirror the API's route structure - Keep API-specific test utilities in the shared test common directory ### Testing Configuration - Use cargo features to make running specific test groups easier - Consider using cargo-nextest for parallel test execution - Add test tags to group tests by functionality or component ### Running Tests in Different Scopes - **Run all tests**: `cargo test` - **Run library-specific tests**: `cargo test -p ` - **Run a specific test**: `cargo test ` - **Run tests with specific pattern**: `cargo test -- ` - **Run tests conditionally**: Use features to enable/disable test categories ```toml # In Cargo.toml [features] integration-tests = [] unit-tests = [] ``` ```rust // In code #[cfg(test)] #[cfg(feature = "integration-tests")] mod integration_tests { // Test code... } ``` Then, run: `cargo test --features integration-tests` ### Examples of Proper Test Organization #### Example: Library Unit Tests (`#[cfg(test)]` Module) ```rust // File: libs/braintrust/src/helpers.rs // Implementation code... /// Get system message from a stored prompt pub async fn get_prompt_system_message(client: &BraintrustClient, prompt_id: &str) -> Result { // Function implementation... } #[cfg(test)] mod tests { use super::*; use std::env; use dotenv::dotenv; #[tokio::test] async fn test_get_prompt_system_message() -> Result<()> { // Load environment variables dotenv().ok(); // Skip test if no API key is available (non-blocking) if env::var("BRAINTRUST_API_KEY").is_err() { println!("Skipping test_get_prompt_system_message: No API key available"); return Ok(()); } // Test implementation... Ok(()) } } ``` #### Example: Library Integration Tests (Separate Directory) ```rust // File: libs/handlers/tests/metrics/delete_metric_test.rs use handlers::metrics::delete_metric_handler; use database::models::MetricFile; use crate::common::fixtures::create_test_metric; use anyhow::Result; /// Integration test for the delete_metric_handler #[tokio::test] async fn test_delete_metric_integration() -> Result<()> { // Setup test environment let test_db = setup_test_db().await?; // Create and insert test data let test_metric = create_test_metric(&test_db).await?; // Test the handler functionality let result = delete_metric_handler(&test_metric.id, &test_db.user_id).await?; // Verify results assert!(result.deleted_at.is_some()); // Clean up test data test_db.cleanup().await?; Ok(()) } ``` #### Example: Library-Specific Test Utilities ```rust // File: libs/database/src/test_utils.rs use crate::models::{User, Organization}; use crate::pool::PgPool; use uuid::Uuid; use anyhow::Result; /// Test utilities specific to the database library pub struct DatabaseTestUtils { pub pool: PgPool, pub test_id: String, } impl DatabaseTestUtils { pub async fn new() -> Result { // Initialize test database connection let pool = create_test_pool().await?; let test_id = Uuid::new_v4().to_string(); Ok(Self { pool, test_id }) } pub async fn create_test_user(&self) -> Result { // Create a test user in the database // ... } pub async fn cleanup(&self) -> Result<()> { // Clean up test data // ... } } ``` ## General Testing Guidelines - All tests must be async and use tokio test framework - Tests should be well-documented with clear test case descriptions and expected outputs - Each test should focus on testing a single piece of functionality - Tests should be independent and not rely on the state of other tests - Use meaningful test names that describe what is being tested - **Design for testability**: Code should be written with testing in mind - Favor dependency injection for easier mocking - Separate business logic from external dependencies - Use interfaces/traits to abstract dependencies - Write small, focused functions that do one thing well - Avoid global state and side effects where possible ## Unit Tests - Unit tests should be inline with the code they are testing using `#[cfg(test)]` modules - Each public function should have corresponding unit tests - **Important**: Unit tests should NEVER connect to external services or databases - Mock all external dependencies: - Use mockito for HTTP services - Use trait-based mocks for database operations - Create mock implementations of dependencies - **IMPORTANT**: Always use the async version of mockito: ```rust // Correct async approach let server = mockito::Server::new_async().await; ``` Instead of: ```rust // Incorrect: Not compatible with tokio runtime let server = mockito::Server::new(); ``` - When using mockito in async tests, ensure all mock setup is done asynchronously - Test both success and error cases - Test edge cases and boundary conditions - Structure unit tests to maximize code coverage - Use dependency injection to make code easily testable with mocks ## Integration Tests Integration tests are specifically designed to verify the interaction with external services and should be separate from the main codebase. ### Integration Test Structure - **Location**: Integration tests should always be in a separate `tests/` directory, never mixed with the main code - **External Interaction**: Unlike unit tests, integration tests are explicitly designed to interact with external services (databases, APIs, etc.) - **Configuration**: - Configuration should come from environment variables via `.env` files - Use `dotenv` to load environment variables during test setup - Example: Database connection parameters should come from `.env` - Prefer `.env.test` for test-specific configurations ```rust // Example of proper integration test setup use dotenv::dotenv; use diesel_async::AsyncPgConnection; use diesel_async::pooled_connection::AsyncDieselConnectionManager; use deadpool_diesel::postgres::Pool; // Setup function that loads from environment async fn setup_test_environment() -> Result { // Load environment variables, preferring .env.test if available if std::path::Path::new(".env.test").exists() { dotenv::from_filename(".env.test").ok(); } else { dotenv().ok(); } // Create database pool from environment variables let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set for integration tests"); let config = AsyncDieselConnectionManager::::new(database_url); let pool = Pool::builder(config).max_size(5).build()?; // Return test context with real connections to external services Ok(TestContext { pool, ... }) } ``` ### Library-Specific Integration Tests - Each library should have its own integration tests in its `tests/` directory - These tests should focus ONLY on the library's public API - Integration tests should NOT cross library boundaries - Example structure: ``` libs/ ├── database/ │ ├── src/ │ └── tests/ # Database-specific integration tests │ ├── common/ # Database test utilities │ └── models/ # Tests for database models ├── handlers/ │ ├── src/ │ └── tests/ # Handler-specific integration tests │ ├── common/ # Handler test utilities │ └── users/ # Tests for user handlers ``` ### API Integration Tests - API tests should be in the main `/api/tests/` directory - Structure should mirror the API routes - Focus on end-to-end testing of the entire API - Example structure: ``` api/ ├── src/ └── tests/ ├── common/ # Shared API test utilities └── integration/ # Organized by API resource ├── users/ # User endpoint tests ├── threads/ # Thread endpoint tests └── messages/ # Message endpoint tests ``` ### Modular Test Utilities - **Scope Isolation**: Test utilities should be scoped to their specific domain: - Database utilities in `libs/database/tests/common/` - Handler utilities in `libs/handlers/tests/common/` - API utilities in `/api/tests/common/` - **Modular Structure**: Organize test utilities by function: ``` tests/common/ ├── mod.rs # Re-exports common utilities ├── db.rs # Database test helpers ├── http.rs # HTTP client testing ├── auth.rs # Authentication test helpers └── fixtures/ # Test data fixtures ├── mod.rs # Re-exports all fixtures ├── users.rs # User test data └── messages.rs # Message test data ``` - **No Global Utilities**: Avoid creating global test utilities shared across all components - **Library-Specific Fixtures**: Test fixtures should be specific to their domain ### Test Data Isolation - Each test should create its own isolated data - Use unique identifiers (UUIDs) to mark test data - Clean up after tests complete - Use test_id pattern to track and clean up test data: ```rust #[tokio::test] async fn test_user_creation() -> Result<()> { let test_id = Uuid::new_v4().to_string(); let pool = get_db_pool().await?; // Create test data with test_id marker let test_user = UserBuilder::new() .with_email("test@example.com") .with_test_id(&test_id) // Mark this as test data .build(); // Insert test data test_user.insert(&pool).await?; // Run test assertions... // Clean up all data with this test_id cleanup_test_data(&pool, &test_id).await?; Ok(()) } ``` ### Integration Test Setup Best Practices #### Environment Configuration - Create a centralized test environment setup system: ```rust // tests/common/env.rs use std::sync::Once; use dotenv::dotenv; static ENV_SETUP: Once = Once::new(); /// Initialize test environment once per test process pub fn init_test_env() { ENV_SETUP.call_once(|| { // First check for .env.test if std::path::Path::new(".env.test").exists() { dotenv::from_filename(".env.test").ok(); } else { // Fall back to regular .env dotenv().ok(); } // Set additional test-specific env vars if std::env::var("TEST_ENV").is_err() { std::env::set_var("TEST_ENV", "test"); } // Initialize logger for tests if std::env::var("TEST_LOG").is_ok() { tracing_subscriber::fmt() .with_env_filter("debug") .with_test_writer() .init(); } }); } ``` #### Database Setup - All integration tests must import and utilize the application's schema from [schema.rs](mdc:src/database/schema.rs) - Database models from [models.rs](mdc:src/database/models.rs) should be used for test data setup and verification - Create a robust database test helper: ```rust // tests/common/db.rs use anyhow::Result; use diesel_async::{AsyncPgConnection, AsyncConnection}; use diesel_async::pooled_connection::AsyncDieselConnectionManager; use deadpool_diesel::postgres::Pool; use uuid::Uuid; pub struct TestDb { pub pool: Pool, pub test_id: String, // Unique identifier for this test run } impl TestDb { /// Creates a new isolated test database environment pub async fn new() -> Result { // Initialize environment crate::common::env::init_test_env(); // Generate unique test identifier let test_id = Uuid::new_v4().to_string(); // Get database config from env let database_url = std::env::var("TEST_DATABASE_URL") .expect("TEST_DATABASE_URL must be set for tests"); // Create connection manager and pool let config = AsyncDieselConnectionManager::::new(database_url); let pool = Pool::builder(config) .max_size(5) .build()?; let db = Self { pool, test_id }; // Setup initial test data db.setup_schema().await?; Ok(db) } /// Setup schema and initial test data async fn setup_schema(&self) -> Result<()> { let conn = &mut self.pool.get().await?; // Run migrations or setup tables // Add any shared setup like creating test users Ok(()) } /// Clean up test data created by this test instance pub async fn cleanup(&self) -> Result<()> { let conn = &mut self.pool.get().await?; // Delete data with test_id marker // Reset sequences if needed Ok(()) } /// Create test data with proper isolation using the test_id pub async fn create_test_data(&self) -> Result<()> { // Create test data tagged with test_id for isolation Ok(()) } } impl Drop for TestDb { fn drop(&mut self) { // Optionally perform synchronous cleanup on drop // This ensures cleanup even if tests panic } } ``` #### Required Environment Variables Create a `.env.test` file with necessary test configurations: ```env TEST_DATABASE_URL=postgres://user:pass@localhost/test_db TEST_API_KEY=test-key TEST_ENV=test # Optional: enable test logging # TEST_LOG=debug ``` **IMPORTANT**: All integration tests must have all required services configured and accessible via environment variables loaded through dotenv. Tests should never be written with hardcoded service configurations. #### Service Mocks Create reusable mock services for common external dependencies: ```rust // tests/common/mocks/http_client.rs pub struct MockHttpClient { server: mockito::Server, } impl MockHttpClient { pub async fn new() -> Self { Self { server: mockito::Server::new_async().await, } } pub fn url(&self) -> String { self.server.url() } pub fn mock_success_response(&self, path: &str, body: &str) -> mockito::Mock { self.server .mock("GET", path) .with_status(200) .with_header("content-type", "application/json") .with_body(body) .create() } pub fn mock_error_response(&self, path: &str, status: usize) -> mockito::Mock { self.server .mock("GET", path) .with_status(status) .create() } } ``` ## Test Structure ```rust #[cfg(test)] mod tests { use super::*; use mockito; use tokio; // Optional: Setup function for common test initialization async fn setup() -> TestContext { // Setup code here } #[tokio::test] async fn test_name() { // Test case description in comments // Expected output in comments // Arrange // Setup test data and dependencies // Act // Execute the function being tested // Assert // Verify the results } } ``` ## Mocking Guidelines for Async Testing ### Mockito for HTTP Service Mocking Mockito is the primary tool for mocking HTTP services in tests. When using mockito in an async context: ```rust // CORRECT: Create async mockito server #[tokio::test] async fn test_http_client() -> Result<()> { // Always use the async version for tokio compatibility let server = mockito::Server::new_async().await; // Setup the mock with expected request and response let mock = server .mock("GET", "/api/data") .with_status(200) .with_header("content-type", "application/json") .with_body(r#"{"key": "value"}"#) .create(); // Use the server's URL with your client let client = YourHttpClient::new(&server.url()); let response = client.get_data().await?; // Verify the mock was called as expected mock.assert(); Ok(()) } ``` #### Mockito Best Practices: - **Always use `Server::new_async()`** instead of `Server::new()` for tokio compatibility - **Match complex requests** with matchers: ```rust // Match JSON request bodies server.mock("POST", "/api/users") .match_body(mockito::Matcher::Json(json!({"name": "Test User"}))) .with_status(201) .create(); // Match headers server.mock("GET", "/api/protected") .match_header("authorization", "Bearer token") .with_status(200) .create(); ``` - **Test different response scenarios** (success, errors, timeouts): ```rust // Success response let success_mock = server.mock("GET", "/api/resource/1") .with_status(200) .with_body(r#"{"id": "1", "name": "Resource"}"#) .create(); // Error response let error_mock = server.mock("GET", "/api/resource/999") .with_status(404) .with_body(r#"{"error": "Not found"}"#) .create(); // Timeout simulation let timeout_mock = server.mock("GET", "/api/slow") .with_delay(std::time::Duration::from_secs(10)) .with_status(200) .create(); ``` - **Create reusable mock setup functions** for common patterns ### Additional Mocking Libraries for Async Rust #### 1. Mock_it for trait mocking For mocking traits and interfaces: ```rust use mock_it::Mock; #[async_trait] pub trait Database { async fn get_user(&self, id: &str) -> Result; } // Create a mock implementation let mut db_mock = Mock::new(); // Set up expectations with async behavior db_mock.expect_call(matching!("get_user", arg if arg == "123")) .returns_async(Ok(User { id: "123".to_string(), name: "Test User".to_string(), })); // Test using the mock let result = db_mock.get_user("123").await?; assert_eq!(result.name, "Test User"); ``` #### 2. Async-std's Async-Mock For async-std runtime testing: ```rust use async_std::test; use async_mock::AsyncMock; #[derive(AsyncMock)] #[async_mock(UserRepository)] pub trait UserRepository { async fn find_user(&self, id: &str) -> Result; } #[test] async fn test_user_service() { // Create a mock repository let mock_repo = MockUserRepository::new(); // Setup expectations mock_repo.expect_find_user() .with(eq("123")) .returns(Ok(User { id: "123".to_string(), name: "User" })); // Test service with mock let service = UserService::new(mock_repo); let user = service.get_user_data("123").await?; assert_eq!(user.name, "User"); } ``` #### 3. WireMock for Complex HTTP Mocking For extensive API mocking scenarios: ```rust use wiremock::{MockServer, Mock, ResponseTemplate}; use wiremock::matchers::{method, path}; #[tokio::test] async fn test_api_client() -> Result<()> { // Start mock server let mock_server = MockServer::start().await; // Setup mock Mock::given(method("GET")) .and(path("/api/users")) .respond_with(ResponseTemplate::new(200) .set_body_json(json!([ {"id": "1", "name": "User 1"}, {"id": "2", "name": "User 2"} ])) ) .mount(&mock_server) .await; // Create client using mock URL let client = ApiClient::new(&mock_server.uri()); // Test the client let users = client.list_users().await?; assert_eq!(users.len(), 2); Ok(()) } ``` #### 4. Tower's `mock` Module for Service Mocking For mocking Tower services: ```rust use tower_test::{mock, assert_request_eq}; use tower::Service; #[tokio::test] async fn test_tower_service() { // Create mock service let (mut mock, handle) = mock::pair::, Response<()>>(); // Spawn a task that processes requests tokio::spawn(async move { // Send successful response if let Ok(request) = handle.recv().await { handle.send_response( Response::builder() .status(200) .body(()) .unwrap() ); } }); // Create your service that uses the mock let service = YourService::new(mock); // Make request through your service let response = service .call(Request::get("/test").body(()).unwrap()) .await .unwrap(); assert_eq!(response.status(), 200); } ``` ### Mocking Database Access for Unit Tests For unit tests, completely mock the database access rather than using real connections: ```rust use mock_it::Mock; use async_trait::async_trait; use uuid::Uuid; // Define a repository trait that can be mocked #[async_trait] pub trait UserRepository { async fn find_user_by_id(&self, id: &str) -> Result>; async fn create_user(&self, email: &str, name: &str) -> Result; async fn delete_user(&self, id: &str) -> Result<()>; } // In unit tests, create a mock implementation #[tokio::test] async fn test_user_service_with_mocked_db() -> Result<()> { // Create mock repository let mut repo_mock = Mock::new(); // Setup expectations for database operations let test_user = User { id: Uuid::new_v4().to_string(), email: "test@example.com".to_string(), name: "Test User".to_string(), created_at: chrono::Utc::now(), }; // Mock the find_user_by_id method repo_mock.expect_call(matching!("find_user_by_id", arg if arg == "123")) .returns_async(Ok(Some(test_user.clone()))); // Create the service with the mocked repository let service = UserService::new(repo_mock); // Test the service logic let user = service.get_user_profile("123").await?; // Assertions to verify service logic assert_eq!(user.email, "test@example.com"); Ok(()) } ``` ### Mocking Diesel Models for Unit Tests For mocking Diesel model operations: ```rust use mockall::predicate::*; use mockall::mock; // Create a mock for database operations mock! { pub DieselDb { async fn find_user(&self, id: &str) -> Result>; async fn create_user(&self, data: NewUser) -> Result; } } // User service that depends on database struct UserService { db: Box, } impl UserService { fn new(db: impl DieselDbTrait + 'static) -> Self { Self { db: Box::new(db) } } async fn get_user(&self, id: &str) -> Result> { self.db.find_user(id).await } } // Unit test with mocked database #[tokio::test] async fn test_get_user() -> Result<()> { // Create mock DB let mut mock_db = MockDieselDb::new(); // Setup expectations let test_user = User { id: "user123".to_string(), name: "Test User".to_string(), email: "test@example.com".to_string(), created_at: chrono::Utc::now(), }; mock_db.expect_find_user() .with(eq("user123")) .times(1) .returning(move |_| Ok(Some(test_user.clone()))); // Create service with mock let service = UserService::new(mock_db); // Test service method let user = service.get_user("user123").await?; // Verify results assert!(user.is_some()); let user = user.unwrap(); assert_eq!(user.email, "test@example.com"); Ok(()) } ``` ### Combining Different Mocking Approaches For complex systems, combine different mocking libraries: ```rust #[tokio::test] async fn test_complex_system() -> Result<()> { // Set up HTTP mock let server = mockito::Server::new_async().await; // Set up mock responses let api_mock = server .mock("GET", "/api/data") .with_status(200) .with_body(r#"{"data": "value"}"#) .create(); // Create mock database implementation let mut db_mock = Mock::new(); db_mock.expect_call(matching!("save_data", _)) .returns_async(Ok(())); // Initialize system with mocks let client = HttpClient::new(&server.url()); let system = YourSystem::new(client, db_mock); // Test system behavior let result = system.process_and_save().await?; assert!(result.is_success()); // Verify HTTP mock was called api_mock.assert(); Ok(()) } ``` ## Error Testing - Test error conditions and error handling - Verify error messages and error types - Test timeout scenarios - Test connection failures - Test invalid input handling ## Database Testing - Use a separate test database for integration tests - Clean up test data after tests complete - Test database connection error handling ## Test Output - Tests should provide clear error messages - Use descriptive assert messages - Print relevant debug information in test failures - Log test execution progress for long-running tests ## CI/CD Considerations - All tests must pass in CI environment - Tests should be reproducible - Tests should not have external dependencies that could fail CI - Test execution time should be reasonable ## Example Unit Test with Mockito ```rust #[cfg(test)] mod tests { use super::*; use mockito; use tokio; use anyhow::Result; #[tokio::test] async fn test_api_call_success() -> Result<()> { // Test case: Successful API call returns expected response // Expected: Response contains user data with status 200 // Create async mockito server let server = mockito::Server::new_async().await; // Setup the mock with expected request and response let mock = server .mock("GET", "/api/user") .match_header("authorization", "Bearer test-token") .with_status(200) .with_body(r#"{"id": "123", "name": "Test User"}"#) .create(); // Create API client with mock server URL let client = ApiClient::new(server.url()); // Execute the function being tested let response = client.get_user("test-token").await?; // Verify results assert_eq!(response.id, "123"); assert_eq!(response.name, "Test User"); // Verify that the mock was called as expected mock.assert(); Ok(()) } #[tokio::test] async fn test_api_call_error_handling() -> Result<()> { // Test case: API returns error status // Expected: Function returns appropriate error let server = mockito::Server::new_async().await; // Setup mock with error response let mock = server .mock("GET", "/api/user") .with_status(401) .with_body(r#"{"error": "Unauthorized"}"#) .create(); let client = ApiClient::new(server.url()); // Execute and verify it returns an error let result = client.get_user("invalid-token").await; assert!(result.is_err()); // Verify the error contains the expected message let err = result.unwrap_err(); assert!(err.to_string().contains("Unauthorized")); mock.assert(); Ok(()) } } ``` ## Example Application Integration Test ```rust use crate::tests::common::{db::TestDb, env::init_test_env}; use crate::database::{models, schema}; use crate::handlers::users::get_user_handler; use anyhow::Result; #[tokio::test] async fn test_user_creation_flow() -> Result<()> { // Initialize test environment with proper setup init_test_env(); // Setup test database with isolation let test_db = TestDb::new().await?; // Create test user using fixture or builder pattern let test_user = models::User::builder() .email("test@example.com") .name("Test User") .build(); // Insert test data with proper error handling diesel::insert_into(schema::users::table) .values(&test_user) .execute(&mut test_db.pool.get().await?) .await?; // Create HTTP client for API testing let client = reqwest::Client::new(); // Test the API endpoint let response = client .get(&format!("{}/api/users/{}", TEST_API_URL, test_user.id)) .header("Authorization", "Bearer test-token") .send() .await?; // Verify response status assert_eq!(response.status(), 200); // Parse and verify response body let user_response = response.json::().await?; assert_eq!(user_response.email, "test@example.com"); // Clean up test data test_db.cleanup().await?; Ok(()) } ``` ## Example Library Integration Test This example shows how to structure integration tests for libraries in the `libs/` directory: ```rust // File: libs/my_library/tests/integration_test.rs use my_library::{Client, Config}; use anyhow::Result; // Import test utilities if needed // Testing utilities can be in the library's tests/common/ directory #[tokio::test] async fn test_client_performs_operation() -> Result<()> { // Setup mock server for external API let server = mockito::Server::new_async().await; // Configure mock responses let mock = server .mock("POST", "/api/resource") .with_status(201) .with_body(r#"{"id": "new-resource-id"}"#) .create(); // Configure the client to use the mock server let config = Config::builder() .base_url(server.url()) .timeout(std::time::Duration::from_secs(5)) .build(); let client = Client::new(config); // Call the library function being tested let result = client.create_resource("test-resource").await?; // Verify the library correctly processed the response assert_eq!(result.id, "new-resource-id"); // Verify the mock was called as expected mock.assert(); Ok(()) } ``` ## Common Test Utilities A comprehensive set of testing utilities has been implemented in the `tests/common/` directory. These utilities provide a standardized approach to testing throughout the codebase and should be used for all new tests. The utilities address common testing patterns and provide a consistent interface for test code. ### Common Test Module Structure ``` tests/ ├── common/ │ ├── mod.rs # Main module that re-exports all utilities │ ├── env.rs # Environment setup utilities │ ├── db.rs # Database testing utilities with test isolation │ ├── http/ # HTTP testing utilities │ │ ├── mock_server.rs # MockServer wrapper for mockito │ │ ├── client.rs # TestHttpClient for API requests │ │ └── mod.rs # HTTP module exports │ ├── fixtures/ # Test data fixtures │ │ ├── mod.rs # Exports all fixtures │ │ ├── builder.rs # FixtureBuilder trait and helpers │ │ ├── users.rs # User-related test data │ │ └── threads.rs # Thread-related test data │ ├── assertions/ # Test assertion utilities │ │ ├── mod.rs # Exports all assertions │ │ ├── response.rs # HTTP response assertions │ │ └── model.rs # Model validation assertions │ ├── matchers/ # Matcher utilities for mockito │ │ ├── mod.rs # Exports all matchers │ │ ├── json.rs # JSON matchers for request bodies │ │ └── headers.rs # Header matchers for requests │ └── helpers.rs # General test helper functions └── integration/ # Integration test files ``` ### Environment Setup Utilities The `env.rs` module provides utilities for setting up the test environment: ```rust // tests/common/env.rs use std::sync::Once; use dotenv::dotenv; static ENV_SETUP: Once = Once::new(); /// Initialize test environment once per test process pub fn init_test_env() { ENV_SETUP.call_once(|| { // First check for .env.test if std::path::Path::new(".env.test").exists() { dotenv::from_filename(".env.test").ok(); } else { // Fall back to regular .env dotenv().ok(); } // Set additional test-specific env vars if std::env::var("TEST_ENV").is_err() { std::env::set_var("TEST_ENV", "test"); } // Initialize logger for tests if std::env::var("TEST_LOG").is_ok() { tracing_subscriber::fmt() .with_env_filter("debug") .with_test_writer() .init(); } }); } /// Get a config value from environment with fallback pub fn get_test_config(key: &str, default: &str) -> String { std::env::var(key).unwrap_or_else(|_| default.to_string()) } ``` ### Database Testing Utilities The `db.rs` module provides utilities for database testing with proper test isolation: ```rust // tests/common/db.rs use anyhow::Result; use diesel_async::{AsyncPgConnection, AsyncConnection}; use diesel_async::pooled_connection::AsyncDieselConnectionManager; use deadpool_diesel::postgres::Pool; use uuid::Uuid; pub struct TestDb { pub pool: Pool, pub test_id: String, // Unique identifier for this test run } impl TestDb { /// Creates a new isolated test database environment pub async fn new() -> Result { // Initialize environment crate::common::env::init_test_env(); // Generate unique test identifier let test_id = Uuid::new_v4().to_string(); // Get database config from env let database_url = std::env::var("TEST_DATABASE_URL") .expect("TEST_DATABASE_URL must be set for tests"); // Create connection manager and pool let config = AsyncDieselConnectionManager::::new(database_url); let pool = Pool::builder(config) .max_size(5) .build()?; let db = Self { pool, test_id }; // Optional: Setup initial test data // db.setup_schema().await?; Ok(db) } /// Get a connection from the pool pub async fn get_conn(&self) -> Result { Ok(self.pool.get().await?) } /// Clean up test data created by this test instance pub async fn cleanup(&self) -> Result<()> { // Example: Clean up tables used in tests // let conn = &mut self.get_conn().await?; // diesel::delete(schema::users::table) // .filter(schema::users::test_id.eq(&self.test_id)) // .execute(conn) // .await?; Ok(()) } /// Get the unique test ID for this test instance pub fn test_id(&self) -> &str { &self.test_id } } impl Drop for TestDb { fn drop(&mut self) { // Optional synchronous cleanup fallback } } ``` ### HTTP Testing Utilities The `http/` directory contains utilities for HTTP testing, including a MockServer wrapper and a TestHttpClient: ```rust // tests/common/http/mock_server.rs use anyhow::Result; use mockito::{self, Mock, Server}; use serde_json::Value; /// MockServer is a wrapper around mockito::Server pub struct MockServer { server: Server, } impl MockServer { /// Create a new MockServer pub async fn new() -> Result { Ok(Self { server: Server::new_async().await, }) } /// Get the base URL of the mock server pub fn url(&self) -> String { self.server.url() } /// Mock a GET request with JSON response pub fn mock_get_json(&self, path: &str, response: &T) -> Result { let body = serde_json::to_string(response)?; Ok(self.server .mock("GET", path) .with_status(200) .with_header("content-type", "application/json") .with_body(body) .create()) } /// Mock a POST request with JSON request and response pub fn mock_post_json(&self, path: &str, request_matcher: Option, response: &T) -> Result { let body = serde_json::to_string(response)?; let mut mock = self.server .mock("POST", path) .with_status(200) .with_header("content-type", "application/json") .with_body(body); // Add request body matcher if provided if let Some(req_body) = request_matcher { mock = mock.match_body(crate::common::matchers::json::json_contains(req_body)); } Ok(mock.create()) } /// Mock an error response pub fn mock_error(&self, path: &str, method: &str, status: usize, message: &str) -> Result { let error_body = serde_json::json!({ "error": message, }); Ok(self.server .mock(method, path) .with_status(status) .with_header("content-type", "application/json") .with_body(serde_json::to_string(&error_body)?) .create()) } } // tests/common/http/client.rs use anyhow::Result; use reqwest::{Client, Method, RequestBuilder, Response, StatusCode}; use serde::de::DeserializeOwned; use serde_json::Value; use std::time::Duration; /// TestHttpClient provides a fluent API for making HTTP requests in tests pub struct TestHttpClient { client: Client, base_url: String, } impl TestHttpClient { /// Create a new TestHttpClient with the given base URL pub fn new(base_url: &str) -> Self { let client = Client::builder() .timeout(Duration::from_secs(5)) .build() .expect("Failed to create HTTP client"); Self { client, base_url: base_url.to_string(), } } /// Create a request builder with the given method and path pub fn request(&self, method: Method, path: &str) -> RequestBuilder { let url = format!("{}{}", self.base_url, path); self.client.request(method, url) } /// Make a GET request and return the response pub async fn get(&self, path: &str) -> Result { Ok(self.request(Method::GET, path).send().await?) } /// Make a POST request with the given body and return the response pub async fn post(&self, path: &str, body: &T) -> Result { Ok(self.request(Method::POST, path) .json(body) .send() .await?) } /// Make a GET request and parse the response as JSON pub async fn get_json(&self, path: &str) -> Result { let response = self.get(path).await?; if response.status().is_success() { Ok(response.json::().await?) } else { let status = response.status(); let error_text = response.text().await?; anyhow::bail!("Request failed with status {}: {}", status, error_text) } } /// Make a POST request with the given body and parse the response as JSON pub async fn post_json( &self, path: &str, body: &T ) -> Result { let response = self.post(path, body).await?; if response.status().is_success() { Ok(response.json::().await?) } else { let status = response.status(); let error_text = response.text().await?; anyhow::bail!("Request failed with status {}: {}", status, error_text) } } } ``` ### Fixture Builder Pattern The `fixtures/builder.rs` file provides a builder pattern for creating test fixtures: ```rust // tests/common/fixtures/builder.rs use uuid::Uuid; /// TestFixture is a trait for test fixtures pub trait TestFixture { /// Get the test ID for this fixture fn test_id(&self) -> Option<&str>; /// Set the test ID for this fixture fn with_test_id(self, test_id: &str) -> Self; } /// FixtureBuilder is a trait for building test fixtures pub trait FixtureBuilder { /// Build the fixture fn build(&self) -> T; /// Build the fixture with the given test ID fn build_with_test_id(&self, test_id: &str) -> T; } /// Example builder for a User fixture #[derive(Default)] pub struct UserBuilder { pub email: Option, pub name: Option, pub test_id: Option, } impl UserBuilder { pub fn new() -> Self { Self::default() } pub fn email(mut self, email: &str) -> Self { self.email = Some(email.to_string()); self } pub fn name(mut self, name: &str) -> Self { self.name = Some(name.to_string()); self } pub fn test_id(mut self, test_id: &str) -> Self { self.test_id = Some(test_id.to_string()); self } } impl FixtureBuilder for UserBuilder { fn build(&self) -> User { User { id: Uuid::new_v4(), email: self.email.clone().unwrap_or_else(|| format!("user-{}@example.com", Uuid::new_v4())), name: self.name.clone(), test_id: self.test_id.clone(), created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), } } fn build_with_test_id(&self, test_id: &str) -> User { let mut builder = self.clone(); builder.test_id = Some(test_id.to_string()); builder.build() } } ``` ### Assertion Utilities The `assertions/` directory contains utilities for making assertions in tests: ```rust // tests/common/assertions/response.rs use anyhow::Result; use reqwest::Response; use reqwest::StatusCode; use serde::de::DeserializeOwned; use serde_json::Value; /// Extension trait for reqwest::Response providing assertion methods pub trait ResponseAssertions { /// Assert that the response has the given status code fn assert_status(self, status: StatusCode) -> Self; /// Assert that the response has a success status code (2xx) fn assert_success(self) -> Self; /// Assert that the response has an error status code (4xx or 5xx) fn assert_error(self) -> Self; /// Assert that the response contains the given header fn assert_header(self, name: &str, value: &str) -> Self; /// Assert that the response body contains the given JSON value fn assert_json_contains(self, expected: Value) -> Result; /// Deserialize the response body as JSON and apply the given assertion function fn assert_json(self, assert_fn: F) -> Result where T: DeserializeOwned, F: FnOnce(&T) -> bool; } impl ResponseAssertions for Response { fn assert_status(self, status: StatusCode) -> Self { assert_eq!(self.status(), status, "Expected status code {}, got {}", status, self.status()); self } fn assert_success(self) -> Self { assert!(self.status().is_success(), "Expected success status, got {}", self.status()); self } fn assert_error(self) -> Self { assert!(self.status().is_client_error() || self.status().is_server_error(), "Expected error status, got {}", self.status()); self } fn assert_header(self, name: &str, value: &str) -> Self { let header_value = self.headers().get(name) .expect(&format!("Header {} not found", name)) .to_str() .expect(&format!("Header {} is not valid UTF-8", name)); assert_eq!(header_value, value, "Expected header {} to be {}, got {}", name, value, header_value); self } async fn assert_json_contains(self, expected: Value) -> Result { let json = self.json::().await?; // Check if expected is a subset of json assert!(json_contains(&json, &expected), "Expected JSON to contain {:?}, got {:?}", expected, json); Ok(self) } async fn assert_json(self, assert_fn: F) -> Result where T: DeserializeOwned, F: FnOnce(&T) -> bool, { let json = self.json::().await?; assert!(assert_fn(&json), "JSON assertion failed"); Ok(self) } } // Helper function to check if one JSON value contains another fn json_contains(json: &Value, expected: &Value) -> bool { match (json, expected) { (Value::Object(json_obj), Value::Object(expected_obj)) => { expected_obj.iter().all(|(k, v)| { json_obj.get(k).map_or(false, |json_v| json_contains(json_v, v)) }) } (Value::Array(json_arr), Value::Array(expected_arr)) => { expected_arr.iter().all(|expected_v| { json_arr.iter().any(|json_v| json_contains(json_v, expected_v)) }) } _ => json == expected, } } ``` ### JSON and Header Matchers The `matchers/` directory contains utilities for matching JSON and headers in mockito: ```rust // tests/common/matchers/json.rs use mockito::Matcher; use serde_json::Value; use std::fmt; /// Create a matcher that checks if a JSON request body contains the given JSON value pub fn json_contains(expected: Value) -> JsonContainsMatcher { JsonContainsMatcher { expected } } /// Matcher for checking if a JSON request body contains a JSON value pub struct JsonContainsMatcher { expected: Value, } impl Matcher for JsonContainsMatcher { fn matches(&self, body: &[u8]) -> bool { let actual: Value = match serde_json::from_slice(body) { Ok(v) => v, Err(_) => return false, }; contains_json(&actual, &self.expected) } } impl fmt::Display for JsonContainsMatcher { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "JSON containing {}", self.expected) } } /// Helper function to check if one JSON value contains another fn contains_json(json: &Value, expected: &Value) -> bool { match (json, expected) { (Value::Object(json_obj), Value::Object(expected_obj)) => { expected_obj.iter().all(|(k, v)| { json_obj.get(k).map_or(false, |json_v| contains_json(json_v, v)) }) } (Value::Array(json_arr), Value::Array(expected_arr)) => { expected_arr.iter().all(|expected_v| { json_arr.iter().any(|json_v| contains_json(json_v, expected_v)) }) } _ => json == expected, } } // tests/common/matchers/headers.rs use mockito::Matcher; use std::collections::HashMap; use std::fmt; /// Create a matcher that checks if request headers match the expected headers pub fn header_matcher(expected_headers: HashMap) -> HeaderMatcher { HeaderMatcher { expected_headers } } /// Matcher for checking request headers pub struct HeaderMatcher { expected_headers: HashMap, } impl Matcher for HeaderMatcher { fn matches(&self, request_headers: &[u8]) -> bool { let headers_str = match std::str::from_utf8(request_headers) { Ok(s) => s, Err(_) => return false, }; // Parse headers let headers: HashMap = headers_str .split("\r\n") .filter(|line| !line.is_empty()) .filter_map(|line| { let mut parts = line.splitn(2, ": "); let name = parts.next()?; let value = parts.next()?; Some((name.to_lowercase(), value.to_string())) }) .collect(); // Check if expected headers are present self.expected_headers.iter().all(|(name, value)| { headers.get(&name.to_lowercase()).map_or(false, |v| v == value) }) } } impl fmt::Display for HeaderMatcher { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Headers containing {:?}", self.expected_headers) } } ``` ### Using These Test Utilities Here's an example of using these test utilities in an integration test: ```rust use crate::tests::common::{ db::TestDb, env::init_test_env, http::{MockServer, TestHttpClient}, fixtures::UserBuilder, assertions::ResponseAssertions, }; use anyhow::Result; use serde_json::json; #[tokio::test] async fn test_user_api() -> Result<()> { // Initialize environment init_test_env(); // Setup test database let test_db = TestDb::new().await?; // Create test user using builder let user = UserBuilder::new() .email("test@example.com") .name("Test User") .build_with_test_id(test_db.test_id()); // Insert test user into database // ... // Setup mock server for external API let mock_server = MockServer::new().await?; // Configure mock responses let mock = mock_server.mock_get_json("/api/external", &json!({ "status": "success" }))?; // Create test HTTP client let client = TestHttpClient::new("http://localhost:8000"); // Test the API endpoint let response = client.get(&format!("/api/users/{}", user.id)).await?; // Assert on the response response .assert_status(StatusCode::OK) .assert_header("content-type", "application/json") .assert_json_contains(json!({ "id": user.id.to_string(), "email": "test@example.com" }))?; // Verify the mock was called mock.assert(); // Clean up test_db.cleanup().await?; Ok(()) } ``` ```