Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing

Testing is a crucial part of the Oxidite framework, providing comprehensive tools for unit testing, integration testing, and end-to-end testing. This chapter covers all aspects of testing in Oxidite applications.

Overview

Oxidite provides:

  • Unit testing for individual components
  • Integration testing for routes and middleware
  • End-to-end testing with simulated HTTP requests
  • Test utilities for mocking dependencies
  • Test fixtures and factories
  • Property-based testing support

Setting Up Tests

Basic test setup in your project:

// In your Cargo.toml
[dev-dependencies]
tokio = { version = "1.0", features = ["full"] }
oxidite-testing = "2.0.0"
serial_test = "3.0"

// In your src/lib.rs or src/main.rs
#[cfg(test)]
mod tests {
    use super::*;
    use oxidite_testing::TestServer;
    
    #[tokio::test]
    async fn test_basic_functionality() {
        assert_eq!(2 + 2, 4);
    }
}

Unit Testing

Test individual functions and components:

use oxidite::prelude::*;

// Function to test
pub fn calculate_discount(price: f64, discount_percent: f64) -> f64 {
    if discount_percent <= 0.0 || discount_percent > 100.0 {
        return price;
    }
    
    price * (1.0 - discount_percent / 100.0)
}

pub fn is_valid_email(email: &str) -> bool {
    email.contains('@') && email.contains('.') && email.len() > 5
}

#[cfg(test)]
mod unit_tests {
    use super::*;

    #[test]
    fn test_calculate_discount() {
        assert_eq!(calculate_discount(100.0, 10.0), 90.0);
        assert_eq!(calculate_discount(50.0, 20.0), 40.0);
        assert_eq!(calculate_discount(100.0, 0.0), 100.0);
        assert_eq!(calculate_discount(100.0, 100.0), 0.0);
    }

    #[test]
    fn test_calculate_discount_edge_cases() {
        assert_eq!(calculate_discount(100.0, -10.0), 100.0); // Invalid discount
        assert_eq!(calculate_discount(100.0, 150.0), 100.0); // Too high discount
        assert_eq!(calculate_discount(0.0, 50.0), 0.0); // Zero price
    }

    #[test]
    fn test_is_valid_email() {
        assert!(is_valid_email("user@example.com"));
        assert!(is_valid_email("test.user@domain.co.uk"));
        assert!(!is_valid_email("invalid-email"));
        assert!(!is_valid_email("missing@dot"));
        assert!(!is_valid_email("short@x"));
    }
}

Integration Testing

Test routes and middleware integration:

use oxidite::prelude::*;
use oxidite_testing::{TestServer, RequestBuilder};

// Sample route handler
async fn hello_handler(_req: Request) -> Result<Response> {
    Ok(Response::json(serde_json::json!({
        "message": "Hello, World!",
        "status": "success"
    })))
}

async fn user_handler(Path(user_id): Path<u32>) -> Result<Response> {
    Ok(Response::json(serde_json::json!({
        "id": user_id,
        "name": format!("User {}", user_id),
        "email": format!("user{}@example.com", user_id)
    })))
}

#[cfg(test)]
mod integration_tests {
    use super::*;

    #[tokio::test]
    async fn test_hello_endpoint() {
        let server = TestServer::new(|router| {
            router.get("/hello", hello_handler);
        }).await;

        let response = server.get("/hello").send().await;
        
        assert_eq!(response.status(), 200);
        
        let json: serde_json::Value = response.json().await;
        assert_eq!(json["message"], "Hello, World!");
        assert_eq!(json["status"], "success");
    }

    #[tokio::test]
    async fn test_user_endpoint() {
        let server = TestServer::new(|router| {
            router.get("/users/:id", user_handler);
        }).await;

        let response = server.get("/users/123").send().await;
        
        assert_eq!(response.status(), 200);
        
        let json: serde_json::Value = response.json().await;
        assert_eq!(json["id"], 123);
        assert_eq!(json["name"], "User 123");
        assert_eq!(json["email"], "user123@example.com");
    }

    #[tokio::test]
    async fn test_not_found() {
        let server = TestServer::new(|router| {
            router.get("/hello", hello_handler);
        }).await;

        let response = server.get("/nonexistent").send().await;
        
        assert_eq!(response.status(), 404);
    }
}

Testing with State and Dependencies

Test routes that use application state:

use oxidite::prelude::*;
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    app_name: String,
    version: String,
}

async fn stateful_handler(
    _req: Request,
    State(state): State<Arc<AppState>>
) -> Result<Response> {
    Ok(Response::json(serde_json::json!({
        "app_name": state.app_name,
        "version": state.version
    })))
}

#[cfg(test)]
mod state_tests {
    use super::*;

    #[tokio::test]
    async fn test_stateful_handler() {
        let app_state = Arc::new(AppState {
            app_name: "Test App".to_string(),
            version: "1.0.0".to_string(),
        });

        let server = TestServer::new(move |router| {
            let state_clone = app_state.clone();
            router.with_state(state_clone);
            router.get("/info", stateful_handler);
        }).await;

        let response = server.get("/info").send().await;
        
        assert_eq!(response.status(), 200);
        
        let json: serde_json::Value = response.json().await;
        assert_eq!(json["app_name"], "Test App");
        assert_eq!(json["version"], "1.0.0");
    }
}

Testing Middleware

Test middleware functionality:

use oxidite::prelude::*;

async fn logging_middleware(req: Request, next: Next) -> Result<Response> {
    println!("Request: {} {}", req.method(), req.uri());
    let response = next.run(req).await?;
    println!("Response: {}", response.status());
    Ok(response)
}

async fn auth_middleware(req: Request, next: Next) -> Result<Response> {
    // Check for auth header
    let auth_header = req.headers()
        .get("authorization")
        .and_then(|hv| hv.to_str().ok());
    
    if auth_header.is_none() {
        return Err(Error::Unauthorized("Missing authorization header".to_string()));
    }
    
    next.run(req).await
}

#[cfg(test)]
mod middleware_tests {
    use super::*;

    #[tokio::test]
    async fn test_auth_middleware_success() {
        let server = TestServer::new(|router| {
            router.get("/protected")
                .middleware(auth_middleware)
                .handler(|_req| async { Ok(Response::text("Protected content".to_string())) });
        }).await;

        let response = server
            .get("/protected")
            .header("Authorization", "Bearer token123")
            .send()
            .await;
        
        assert_eq!(response.status(), 200);
        assert_eq!(response.text().await, "Protected content");
    }

    #[tokio::test]
    async fn test_auth_middleware_failure() {
        let server = TestServer::new(|router| {
            router.get("/protected")
                .middleware(auth_middleware)
                .handler(|_req| async { Ok(Response::text("Protected content".to_string())) });
        }).await;

        let response = server.get("/protected").send().await;
        
        assert_eq!(response.status(), 401);
    }
}

Database Testing

Test database operations with test databases:

use oxidite::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Model, Serialize, Deserialize)]
#[model(table = "test_users")]
pub struct TestUser {
    #[model(primary_key)]
    pub id: i32,
    #[model(unique, not_null)]
    pub email: String,
    #[model(not_null)]
    pub name: String,
}

#[cfg(test)]
mod database_tests {
    use super::*;

    async fn setup_test_db() -> Result<()> {
        // Create test database schema
        // This would typically run migrations or create tables
        Ok(())
    }

    async fn teardown_test_db() -> Result<()> {
        // Clean up test database
        Ok(())
    }

    #[tokio::test]
    async fn test_user_crud_operations() {
        setup_test_db().await.unwrap();

        // Test create
        let user = TestUser {
            id: 0,
            email: "test@example.com".to_string(),
            name: "Test User".to_string(),
        };

        let saved_user = user.save().await.unwrap();
        assert!(!saved_user.id == 0);

        // Test read
        let found_user = TestUser::find_by_id(saved_user.id).await.unwrap().unwrap();
        assert_eq!(found_user.email, "test@example.com");
        assert_eq!(found_user.name, "Test User");

        // Test update
        let mut updated_user = found_user;
        updated_user.name = "Updated Name".to_string();
        let updated_user = updated_user.save().await.unwrap();
        assert_eq!(updated_user.name, "Updated Name");

        // Test delete
        updated_user.delete().await.unwrap();
        let deleted_user = TestUser::find_by_id(updated_user.id).await.unwrap();
        assert!(deleted_user.is_none());

        teardown_test_db().await.unwrap();
    }

    #[tokio::test]
    async fn test_duplicate_email_fails() {
        setup_test_db().await.unwrap();

        let user1 = TestUser {
            id: 0,
            email: "duplicate@example.com".to_string(),
            name: "User 1".to_string(),
        };
        user1.save().await.unwrap();

        let user2 = TestUser {
            id: 0,
            email: "duplicate@example.com".to_string(), // Same email
            name: "User 2".to_string(),
        };
        
        // This should fail due to unique constraint
        let result = user2.save().await;
        assert!(result.is_err());

        teardown_test_db().await.unwrap();
    }
}

Mocking and Test Doubles

Create mocks for external dependencies:

use oxidite::prelude::*;

// Service to be mocked
pub trait EmailService: Send + Sync {
    async fn send_email(&self, to: &str, subject: &str, body: &str) -> Result<(), String>;
}

pub struct RealEmailService;

impl EmailService for RealEmailService {
    async fn send_email(&self, to: &str, subject: &str, body: &str) -> Result<(), String> {
        // Actually send email
        println!("Sending email to: {}, subject: {}, body: {}", to, subject, body);
        Ok(())
    }
}

// Handler that uses the service
async fn contact_handler(
    Json(payload): Json<ContactRequest>,
    State(email_service): State<Arc<dyn EmailService>>
) -> Result<Response> {
    email_service
        .send_email(&payload.email, &payload.subject, &payload.message)
        .await
        .map_err(|e| Error::InternalServerError(e))?;

    Ok(Response::json(serde_json::json!({
        "status": "sent",
        "message": "Email sent successfully"
    })))
}

#[derive(serde::Deserialize)]
struct ContactRequest {
    email: String,
    subject: String,
    message: String,
}

// Mock implementation for testing
pub struct MockEmailService {
    pub sent_emails: std::sync::Arc<tokio::sync::Mutex<Vec<SentEmail>>>,
}

#[derive(Clone)]
pub struct SentEmail {
    pub to: String,
    pub subject: String,
    pub body: String,
}

impl MockEmailService {
    pub fn new() -> Self {
        Self {
            sent_emails: std::sync::Arc::new(tokio::sync::Mutex::new(Vec::new())),
        }
    }
    
    pub async fn get_sent_emails(&self) -> Vec<SentEmail> {
        self.sent_emails.lock().await.clone()
    }
}

#[async_trait::async_trait]
impl EmailService for MockEmailService {
    async fn send_email(&self, to: &str, subject: &str, body: &str) -> Result<(), String> {
        let mut emails = self.sent_emails.lock().await;
        emails.push(SentEmail {
            to: to.to_string(),
            subject: subject.to_string(),
            body: body.to_string(),
        });
        Ok(())
    }
}

#[cfg(test)]
mod mock_tests {
    use super::*;

    #[tokio::test]
    async fn test_contact_handler_with_mock() {
        let mock_service = std::sync::Arc::new(MockEmailService::new());
        let service_clone = mock_service.clone();

        let server = TestServer::new(move |router| {
            router.post("/contact")
                .with_state(service_clone.clone() as Arc<dyn EmailService>)
                .handler(contact_handler);
        }).await;

        let response = server
            .post("/contact")
            .json(&serde_json::json!({
                "email": "user@example.com",
                "subject": "Test Subject",
                "message": "Test message"
            }))
            .send()
            .await;

        assert_eq!(response.status(), 200);
        
        let json: serde_json::Value = response.json().await;
        assert_eq!(json["status"], "sent");

        // Verify email was sent via mock
        let sent_emails = mock_service.get_sent_emails().await;
        assert_eq!(sent_emails.len(), 1);
        assert_eq!(sent_emails[0].to, "user@example.com");
        assert_eq!(sent_emails[0].subject, "Test Subject");
        assert_eq!(sent_emails[0].body, "Test message");
    }
}

Property-Based Testing

Use property-based testing for comprehensive validation:

use oxidite::prelude::*;

// Function to test with property-based testing
pub fn reverse_string(s: &str) -> String {
    s.chars().rev().collect()
}

pub fn is_palindrome(s: &str) -> bool {
    let cleaned: String = s.chars()
        .filter(|c| c.is_alphanumeric())
        .map(|c| c.to_lowercase().next().unwrap())
        .collect();
    
    cleaned == reverse_string(&cleaned)
}

#[cfg(test)]
mod property_tests {
    use super::*;
    use proptest::prelude::*;

    // Test that reversing a string twice gives the original
    proptest! {
        #[test]
        fn test_reverse_twice_is_identity(s in ".*") {
            let reversed_once = reverse_string(&s);
            let reversed_twice = reverse_string(&reversed_once);
            prop_assert_eq!(s, reversed_twice);
        }
    }

    // Test palindrome properties
    proptest! {
        #[test]
        fn test_palindromes(s in "[a-zA-Z]{1,10}") {
            // A string concatenated with its reverse should be a palindrome
            let reversed = reverse_string(&s);
            let palindrome = format!("{}{}", s, reversed);
            prop_assert!(is_palindrome(&palindrome));
        }
    }
}

Test Fixtures and Factories

Create reusable test data:

use oxidite::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Model, Serialize, Deserialize, Clone)]
#[model(table = "test_posts")]
pub struct TestPost {
    #[model(primary_key)]
    pub id: i32,
    #[model(not_null)]
    pub title: String,
    #[model(not_null)]
    pub content: String,
    pub user_id: i32,
}

#[derive(Model, Serialize, Deserialize, Clone)]
#[model(table = "test_comments")]
pub struct TestComment {
    #[model(primary_key)]
    pub id: i32,
    #[model(not_null)]
    pub content: String,
    pub post_id: i32,
    pub user_id: i32,
}

// Test factory for creating test data
pub struct TestFactory;

impl TestFactory {
    pub fn create_user(email: &str, name: &str) -> TestUser {
        TestUser {
            id: 0,
            email: email.to_string(),
            name: name.to_string(),
        }
    }

    pub fn create_post(title: &str, content: &str, user_id: i32) -> TestPost {
        TestPost {
            id: 0,
            title: title.to_string(),
            content: content.to_string(),
            user_id,
        }
    }

    pub fn create_comment(content: &str, post_id: i32, user_id: i32) -> TestComment {
        TestComment {
            id: 0,
            content: content.to_string(),
            post_id,
            user_id,
        }
    }
}

#[cfg(test)]
mod fixture_tests {
    use super::*;

    #[tokio::test]
    async fn test_blog_post_with_comments() {
        setup_test_db().await.unwrap();

        // Create test data using factory
        let user = TestFactory::create_user("author@example.com", "Author Name");
        let saved_user = user.save().await.unwrap();

        let post = TestFactory::create_post("Test Post", "Post content", saved_user.id);
        let saved_post = post.save().await.unwrap();

        let comment = TestFactory::create_comment("Great post!", saved_post.id, saved_user.id);
        let saved_comment = comment.save().await.unwrap();

        // Verify relationships
        assert_eq!(saved_comment.post_id, saved_post.id);
        assert_eq!(saved_comment.user_id, saved_user.id);

        // Clean up
        saved_comment.delete().await.unwrap();
        saved_post.delete().await.unwrap();
        saved_user.delete().await.unwrap();

        teardown_test_db().await.unwrap();
    }
}

Test Configuration

Configure test-specific settings:

// In your Cargo.toml
[features]
test_utils = []

// Test utilities module
#[cfg(any(test, feature = "test_utils"))]
pub mod test_utils {
    use oxidite::prelude::*;
    use std::sync::Arc;
    use tokio::sync::Mutex;

    #[derive(Clone)]
    pub struct TestContext {
        pub db_url: String,
        pub temp_dir: tempfile::TempDir,
        pub cleanup_hooks: Arc<Mutex<Vec<Box<dyn FnMut() -> () + Send>>>>,
    }

    impl TestContext {
        pub async fn new() -> Self {
            let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
            Self {
                db_url: format!("sqlite://{}/test.db", temp_dir.path().display()),
                temp_dir,
                cleanup_hooks: Arc::new(Mutex::new(Vec::new())),
            }
        }

        pub async fn add_cleanup_hook<F>(&self, hook: F) 
        where 
            F: FnMut() -> () + Send + 'static 
        {
            let mut hooks = self.cleanup_hooks.lock().await;
            hooks.push(Box::new(hook));
        }

        pub async fn run_cleanup(&self) {
            let mut hooks = self.cleanup_hooks.lock().await;
            for hook in hooks.iter_mut() {
                hook();
            }
        }
    }

    // Test server wrapper with context
    pub struct TestServerWithContext {
        pub server: TestServer,
        pub context: TestContext,
    }

    impl TestServerWithContext {
        pub async fn new<F>(setup_fn: F) -> Self 
        where 
            F: FnOnce(&mut Router, TestContext) + Send + 'static 
        {
            let context = TestContext::new().await;
            let context_clone = context.clone();
            
            let server = TestServer::new(move |router| {
                setup_fn(router, context_clone);
            }).await;

            Self { server, context }
        }
    }
}

#[cfg(test)]
mod configured_tests {
    use super::*;
    use test_utils::*;

    #[tokio::test]
    async fn test_with_context() {
        let test_server = TestServerWithContext::new(|router, _ctx| {
            router.get("/test", |_req| async { 
                Ok(Response::text("Test response".to_string())) 
            });
        }).await;

        let response = test_server.server.get("/test").send().await;
        assert_eq!(response.status(), 200);
        assert_eq!(response.text().await, "Test response");

        test_server.context.run_cleanup().await;
    }
}

Parallel Test Execution

Handle parallel test execution safely:

use oxidite::prelude::*;
use serial_test::serial;

// Use serial_test attribute for tests that can't run in parallel
#[tokio::test]
#[serial]
async fn test_shared_resource() {
    // This test accesses a shared resource and must run serially
    // For example, a test that modifies global configuration
    println!("Running serial test");
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}

#[tokio::test]
async fn test_independent_functionality() {
    // This test can run in parallel with others
    assert_eq!(2 + 2, 4);
}

// Test isolation utilities
pub mod test_isolation {
    use std::sync::atomic::{AtomicUsize, Ordering};

    static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);

    pub fn get_unique_test_id() -> String {
        let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
        format!("test_{}", id)
    }

    pub fn get_unique_table_name() -> String {
        format!("test_table_{}", get_unique_test_id())
    }

    pub fn get_unique_db_name() -> String {
        format!("test_db_{}.db", get_unique_test_id())
    }
}

#[cfg(test)]
mod isolated_tests {
    use super::*;
    use test_isolation::*;

    #[tokio::test]
    async fn test_with_unique_resources() {
        let unique_id = get_unique_test_id();
        let table_name = get_unique_table_name();
        
        println!("Using unique resources: {} - {}", unique_id, table_name);
        
        // Test using isolated resources
        assert!(table_name.starts_with("test_table_test_"));
    }
}

Test Coverage

Measure and improve test coverage:

// In your .cargo/config.toml
// [target.'cfg(coverage)']
// rustflags = ["-Zinstrument-coverage"]

use oxidite::prelude::*;

// Complex function to test thoroughly
pub fn process_order(
    amount: f64,
    tax_rate: f64,
    discount_percent: f64,
    shipping_cost: f64,
    is_international: bool
) -> Result<OrderSummary, String> {
    if amount <= 0.0 {
        return Err("Amount must be positive".to_string());
    }
    
    if tax_rate < 0.0 || tax_rate > 1.0 {
        return Err("Tax rate must be between 0 and 1".to_string());
    }
    
    if discount_percent < 0.0 || discount_percent > 100.0 {
        return Err("Discount percent must be between 0 and 100".to_string());
    }
    
    let discount_amount = amount * (discount_percent / 100.0);
    let subtotal = amount - discount_amount;
    let tax_amount = subtotal * tax_rate;
    let total = subtotal + tax_amount + shipping_cost;
    
    let international_fee = if is_international { total * 0.05 } else { 0.0 };
    let final_total = total + international_fee;

    Ok(OrderSummary {
        subtotal,
        tax_amount,
        shipping_cost,
        discount_amount,
        international_fee,
        total: final_total,
    })
}

#[derive(Debug, PartialEq)]
pub struct OrderSummary {
    pub subtotal: f64,
    pub tax_amount: f64,
    pub shipping_cost: f64,
    pub discount_amount: f64,
    pub international_fee: f64,
    pub total: f64,
}

#[cfg(test)]
mod coverage_tests {
    use super::*;

    #[test]
    fn test_process_order_normal_case() {
        let result = process_order(100.0, 0.1, 10.0, 5.0, false).unwrap();
        
        assert_eq!(result.subtotal, 90.0); // 100 - 10% discount
        assert_eq!(result.tax_amount, 9.0); // 90 * 10% tax
        assert_eq!(result.shipping_cost, 5.0);
        assert_eq!(result.discount_amount, 10.0);
        assert_eq!(result.international_fee, 0.0);
        assert_eq!(result.total, 104.0); // 90 + 9 + 5 + 0
    }

    #[test]
    fn test_process_order_international() {
        let result = process_order(100.0, 0.1, 0.0, 5.0, true).unwrap();
        
        assert_eq!(result.subtotal, 100.0);
        assert_eq!(result.tax_amount, 10.0);
        assert_eq!(result.international_fee, 5.75); // (100 + 10 + 5) * 5%
        assert_eq!(result.total, 120.75);
    }

    #[test]
    fn test_process_order_zero_values() {
        let result = process_order(100.0, 0.0, 0.0, 0.0, false).unwrap();
        
        assert_eq!(result.subtotal, 100.0);
        assert_eq!(result.tax_amount, 0.0);
        assert_eq!(result.total, 100.0);
    }

    #[test]
    fn test_process_order_edge_cases() {
        // Test with very small values
        let result = process_order(0.01, 0.01, 0.01, 0.01, false).unwrap();
        assert!(result.total > 0.0);
        
        // Test maximum values within bounds
        let result = process_order(1000000.0, 0.99, 99.99, 1000.0, true).unwrap();
        assert!(result.total > 0.0);
    }

    #[test]
    fn test_process_order_errors() {
        // Test negative amount
        assert!(process_order(-1.0, 0.1, 10.0, 5.0, false).is_err());
        
        // Test invalid tax rate
        assert!(process_order(100.0, -0.1, 10.0, 5.0, false).is_err());
        assert!(process_order(100.0, 1.5, 10.0, 5.0, false).is_err());
        
        // Test invalid discount percent
        assert!(process_order(100.0, 0.1, -1.0, 5.0, false).is_err());
        assert!(process_order(100.0, 0.1, 101.0, 5.0, false).is_err());
    }
}

Test Reporting

Generate test reports and summaries:

use oxidite::prelude::*;

// Test result aggregator
#[derive(Default)]
pub struct TestResults {
    pub passed: usize,
    pub failed: usize,
    pub ignored: usize,
    pub measured: usize,
}

impl TestResults {
    pub fn add_result(&mut self, result: TestResult) {
        match result.status {
            TestStatus::Passed => self.passed += 1,
            TestStatus::Failed => self.failed += 1,
            TestStatus::Ignored => self.ignored += 1,
            TestStatus::Measured => self.measured += 1,
        }
    }
    
    pub fn total(&self) -> usize {
        self.passed + self.failed + self.ignored + self.measured
    }
    
    pub fn success_rate(&self) -> f64 {
        if self.total() == 0 {
            0.0
        } else {
            (self.passed as f64 / self.total() as f64) * 100.0
        }
    }
    
    pub fn print_summary(&self) {
        println!("Test Results Summary:");
        println!("  Total: {}", self.total());
        println!("  Passed: {} ({:.1}%)", self.passed, self.success_rate());
        println!("  Failed: {}", self.failed);
        println!("  Ignored: {}", self.ignored);
        println!("  Measured: {}", self.measured);
    }
}

pub struct TestResult {
    pub name: String,
    pub status: TestStatus,
    pub duration: std::time::Duration,
    pub error: Option<String>,
}

pub enum TestStatus {
    Passed,
    Failed,
    Ignored,
    Measured,
}

// Example of integrating with a test runner
pub struct TestRunner {
    pub results: TestResults,
}

impl TestRunner {
    pub fn new() -> Self {
        Self {
            results: TestResults::default(),
        }
    }
    
    pub async fn run_test<F>(&mut self, name: &str, test_fn: F) 
    where 
        F: std::future::Future<Output = Result<(), String>> 
    {
        let start = std::time::Instant::now();
        
        match test_fn.await {
            Ok(()) => {
                let result = TestResult {
                    name: name.to_string(),
                    status: TestStatus::Passed,
                    duration: start.elapsed(),
                    error: None,
                };
                self.results.add_result(result);
            }
            Err(error) => {
                let result = TestResult {
                    name: name.to_string(),
                    status: TestStatus::Failed,
                    duration: start.elapsed(),
                    error: Some(error),
                };
                self.results.add_result(result);
            }
        }
    }
}

#[cfg(test)]
mod runner_tests {
    use super::*;

    #[tokio::test]
    async fn test_runner_functionality() {
        let mut runner = TestRunner::new();
        
        // Run a passing test
        runner.run_test("passing_test", async { Ok(()) }).await;
        
        // Run a failing test
        runner.run_test("failing_test", async { 
            Err("Test failed intentionally".to_string()) 
        }).await;
        
        // Run another passing test
        runner.run_test("another_passing_test", async { Ok(()) }).await;
        
        assert_eq!(runner.results.passed, 2);
        assert_eq!(runner.results.failed, 1);
        assert_eq!(runner.results.total(), 3);
        
        let success_rate = runner.results.success_rate();
        assert_eq!(success_rate, 66.66666666666666);
    }
}

Summary

Testing in Oxidite provides comprehensive tools for:

  • Unit Testing: Individual function and component testing
  • Integration Testing: Route and middleware integration
  • Database Testing: ORM and database operation testing
  • Mocking: External dependency simulation
  • Property-Based Testing: Comprehensive validation
  • Fixtures: Reusable test data creation
  • Parallel Execution: Safe concurrent test running
  • Coverage Analysis: Thorough testing measurement
  • Reporting: Detailed test results and summaries

Following testing best practices ensures reliable, maintainable Oxidite applications with high quality and confidence in code changes.