🦀 Testing Framework in TuskLang for Rust

Rust Documentation

Testing Framework in TuskLang for Rust

The testing framework in TuskLang for Rust provides comprehensive testing capabilities that integrate seamlessly with Rust's built-in testing ecosystem, offering powerful assertions, mocking, property-based testing, and performance benchmarking.

Basic Testing Structure

// Basic test module
#[cfg(test)]
mod tests {
    use super::*;
    use tusk_testing::*;

#[test] fn test_basic_functionality() { let result = @calculator.add(2, 3); assert_eq!(result, 5); }

#[tokio::test] async fn test_async_functionality() { let user = @user_service.find_user(1).await.unwrap(); assert!(user.is_some()); assert_eq!(user.unwrap().name, "John Doe"); }

#[test] fn test_with_tusk_operators() { let config = @config.load("test_config.tsk"); let db_url = @env.get("TEST_DATABASE_URL", "sqlite::memory:"); assert_eq!(config.get("app_name"), "TestApp"); assert!(!db_url.is_empty()); } }

Test Configuration and Setup

// Test configuration with TuskLang
pub struct TestConfig {
    pub database_url: String,
    pub api_base_url: String,
    pub test_data_path: String,
}

impl TestConfig { pub fn from_tusk_config() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { let config = @config.load("test_config.tsk"); Ok(TestConfig { database_url: @env.get("TEST_DATABASE_URL", "sqlite::memory:"), api_base_url: config.get("api.base_url", "http://localhost:8080"), test_data_path: config.get("test.data_path", "./test_data"), }) } pub fn setup_test_environment(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { // Setup test database @db.connect(&self.database_url)?; @db.migrate()?; // Load test data let test_data = @file.read(&self.test_data_path)?; @db.seed(&test_data)?; // Setup test API server @api.start(&self.api_base_url)?; Ok(()) } pub fn cleanup_test_environment(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { @db.cleanup()?; @api.stop()?; Ok(()) } }

// Test fixtures pub struct TestFixture { config: TestConfig, test_data: serde_json::Value, }

impl TestFixture { pub async fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { let config = TestConfig::from_tusk_config()?; config.setup_test_environment()?; let test_data = @file.read_json(&format!("{}/fixtures.json", config.test_data_path))?; Ok(Self { config, test_data }) } pub fn get_test_user(&self) -> User { serde_json::from_value(self.test_data["users"]["test_user"].clone()).unwrap() } pub fn get_test_product(&self) -> Product { serde_json::from_value(self.test_data["products"]["test_product"].clone()).unwrap() } }

impl Drop for TestFixture { fn drop(&mut self) { if let Err(e) = self.config.cleanup_test_environment() { eprintln!("Failed to cleanup test environment: {}", e); } } }

Advanced Assertions

// Custom assertion macros for TuskLang
#[macro_export]
macro_rules! assert_tusk_config {
    ($config:expr, $key:expr, $expected:expr) => {
        let actual = $config.get($key);
        assert_eq!(actual, $expected, "Config key '{}' mismatch", $key);
    };
}

#[macro_export] macro_rules! assert_database_record { ($table:expr, $conditions:expr, $expected_count:expr) => { let count = @db.table($table).where_conditions($conditions).count(); assert_eq!(count, $expected_count, "Database record count mismatch for table '{}'", $table); }; }

#[macro_export] macro_rules! assert_api_response { ($response:expr, $status:expr, $expected_data:expr) => { assert_eq!($response.status(), $status); let response_data: serde_json::Value = $response.json().await.unwrap(); assert_eq!(response_data, $expected_data); }; }

// Usage in tests #[test] fn test_config_assertions() { let config = @config.load("test_config.tsk"); assert_tusk_config!(config, "app.name", "TestApp"); assert_tusk_config!(config, "database.type", "sqlite"); }

#[tokio::test] async fn test_database_assertions() { let fixture = TestFixture::new().await.unwrap(); let user = fixture.get_test_user(); @db.users.insert(&user).await.unwrap(); assert_database_record!("users", &[("email", &user.email)], 1); }

#[tokio::test] async fn test_api_assertions() { let client = reqwest::Client::new(); let response = client.get("http://localhost:8080/api/health").send().await.unwrap(); assert_api_response!(response, 200, serde_json::json!({ "status": "healthy", "timestamp": @date.now() })); }

Mocking and Stubbing

// Mock framework for TuskLang
pub trait Mockable {
    fn mock(&self) -> Mock;
}

pub struct Mock { expectations: std::collections::HashMap<String, MockExpectation>, }

pub struct MockExpectation { pub return_value: Option<serde_json::Value>, pub error: Option<String>, pub call_count: usize, pub expected_calls: usize, }

impl Mock { pub fn new() -> Self { Self { expectations: std::collections::HashMap::new(), } } pub fn expect_call(&mut self, method: &str, return_value: serde_json::Value) -> &mut Self { self.expectations.insert(method.to_string(), MockExpectation { return_value: Some(return_value), error: None, call_count: 0, expected_calls: 1, }); self } pub fn expect_error(&mut self, method: &str, error: String) -> &mut Self { self.expectations.insert(method.to_string(), MockExpectation { return_value: None, error: Some(error), call_count: 0, expected_calls: 1, }); self } pub fn times(&mut self, method: &str, count: usize) -> &mut Self { if let Some(expectation) = self.expectations.get_mut(method) { expectation.expected_calls = count; } self } pub fn verify(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { for (method, expectation) in &self.expectations { if expectation.call_count != expectation.expected_calls { return Err(format!("Method '{}' called {} times, expected {}", method, expectation.call_count, expectation.expected_calls).into()); } } Ok(()) } }

// Mock database pub struct MockDatabase { mock: Mock, }

impl MockDatabase { pub fn new() -> Self { Self { mock: Mock::new() } } pub fn expect_find_user(&mut self, user_id: u32, user: User) -> &mut Self { self.mock.expect_call("find_user", serde_json::to_value(user).unwrap()); self } pub fn expect_find_user_error(&mut self, user_id: u32, error: String) -> &mut Self { self.mock.expect_error("find_user", error); self } }

impl Database for MockDatabase { async fn find_user(&self, user_id: u32) -> Result<Option<User>, Box<dyn std::error::Error + Send + Sync>> { let expectation = self.mock.expectations.get_mut("find_user") .ok_or("No expectation set for find_user")?; expectation.call_count += 1; if let Some(error) = &expectation.error { return Err(error.clone().into()); } if let Some(value) = &expectation.return_value { let user: User = serde_json::from_value(value.clone())?; Ok(Some(user)) } else { Ok(None) } } }

// Usage in tests #[tokio::test] async fn test_with_mocks() { let mut mock_db = MockDatabase::new(); let test_user = User::new(1, "Test User".to_string(), "test@example.com".to_string()); mock_db.expect_find_user(1, test_user.clone()); let user_service = UserService::new(Box::new(mock_db)); let result = user_service.get_user(1).await.unwrap(); assert_eq!(result.unwrap().name, "Test User"); mock_db.mock.verify().unwrap(); }

Property-Based Testing

// Property-based testing with proptest
use proptest::prelude::*;

proptest! { #[test] fn test_user_creation_properties(user_id in 1..1000u32, name in ".", email in ".@.*") { let user = User::new(user_id, name.clone(), email.clone()); // Property 1: User ID should be preserved assert_eq!(user.id, user_id); // Property 2: Name should be preserved assert_eq!(user.name, name); // Property 3: Email should be preserved assert_eq!(user.email, email); // Property 4: User should be valid assert!(user.is_valid()); } #[test] fn test_config_parsing_properties(config_str in r#"[a-zA-Z_][a-zA-Z0-9_]\s=\s*[a-zA-Z0-9_]+"#) { let config = @config.parse(&config_str); // Property 1: Config should be valid assert!(config.is_ok()); // Property 2: Config should contain the parsed key-value pair let config = config.unwrap(); assert!(!config.is_empty()); } #[test] fn test_database_query_properties(query in r#"SELECT\s+[a-zA-Z_][a-zA-Z0-9_]\s+FROM\s+[a-zA-Z_][a-zA-Z0-9_]"#) { let result = @db.execute(&query); // Property 1: Query should execute without syntax errors assert!(result.is_ok() || result.unwrap_err().to_string().contains("table")); } }

// Custom strategies for TuskLang types prop_compose! { fn valid_tusk_config()(keys in prop::collection::vec("[a-zA-Z_][a-zA-Z0-9_]*", 1..10)) -> String { keys.into_iter() .map(|key| format!("{} = {}", key, generate_random_value())) .collect::<Vec<_>>() .join("\n") } }

fn generate_random_value() -> String { use rand::Rng; let mut rng = rand::thread_rng(); match rng.gen_range(0..4) { 0 => format!("\"{}\"", rng.gen::<u32>()), 1 => rng.gen::<u32>().to_string(), 2 => if rng.gen() { "true" } else { "false" }.to_string(), _ => format!("@env.get(\"{}\")", rng.gen::<u32>()), } }

proptest! { #[test] fn test_tusk_config_properties(config_str in valid_tusk_config()) { let config = @config.parse(&config_str); // Property 1: Valid TuskLang config should parse successfully assert!(config.is_ok()); // Property 2: Parsed config should contain all keys let config = config.unwrap(); let lines: Vec<_> = config_str.lines().collect(); for line in lines { if let Some((key, _)) = line.split_once('=') { let key = key.trim(); assert!(config.has_key(key)); } } } }

Integration Testing

// Integration test framework
pub struct IntegrationTestFramework {
    test_server: TestServer,
    test_database: TestDatabase,
    test_cache: TestCache,
}

impl IntegrationTestFramework { pub async fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { let config = @config.load("integration_test_config.tsk"); let test_database = TestDatabase::new(&config.get("database.url")).await?; let test_cache = TestCache::new(&config.get("cache.url")).await?; let test_server = TestServer::new(&config.get("server.port")).await?; Ok(Self { test_server, test_database, test_cache, }) } pub async fn setup_test_data(&self, test_data: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { let data = @file.read_json(test_data)?; // Setup database for (table, records) in data.as_object().unwrap() { for record in records.as_array().unwrap() { @db.table(table).insert(record).await?; } } // Setup cache if let Some(cache_data) = data.get("cache") { for (key, value) in cache_data.as_object().unwrap() { @cache.set(key, value, 3600).await?; } } Ok(()) } pub async fn cleanup_test_data(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { @db.cleanup_all().await?; @cache.clear().await?; Ok(()) } pub async fn make_request(&self, method: &str, path: &str, body: Option<serde_json::Value>) -> Result<reqwest::Response, Box<dyn std::error::Error + Send + Sync>> { let client = reqwest::Client::new(); let url = format!("http://localhost:{}{}", self.test_server.port(), path); let request = match method.to_uppercase().as_str() { "GET" => client.get(&url), "POST" => { let mut req = client.post(&url); if let Some(body_data) = body { req = req.json(&body_data); } req } "PUT" => { let mut req = client.put(&url); if let Some(body_data) = body { req = req.json(&body_data); } req } "DELETE" => client.delete(&url), _ => return Err("Unsupported HTTP method".into()), }; Ok(request.send().await?) } }

// Integration test example #[tokio::test] async fn test_user_api_integration() { let framework = IntegrationTestFramework::new().await.unwrap(); // Setup test data framework.setup_test_data("test_data/users.json").await.unwrap(); // Test user creation let new_user = serde_json::json!({ "name": "Integration Test User", "email": "integration@test.com" }); let response = framework.make_request("POST", "/api/users", Some(new_user)).await.unwrap(); assert_eq!(response.status(), 201); let created_user: User = response.json().await.unwrap(); assert_eq!(created_user.name, "Integration Test User"); // Test user retrieval let response = framework.make_request("GET", &format!("/api/users/{}", created_user.id), None).await.unwrap(); assert_eq!(response.status(), 200); let retrieved_user: User = response.json().await.unwrap(); assert_eq!(retrieved_user.id, created_user.id); // Cleanup framework.cleanup_test_data().await.unwrap(); }

Performance Testing

// Performance testing framework
pub struct PerformanceTestFramework {
    metrics: std::sync::Arc<tokio::sync::Mutex<Vec<PerformanceMetric>>>,
}

#[derive(Clone)] pub struct PerformanceMetric { pub test_name: String, pub duration: std::time::Duration, pub memory_usage: usize, pub cpu_usage: f64, }

impl PerformanceTestFramework { pub fn new() -> Self { Self { metrics: std::sync::Arc::new(tokio::sync::Mutex::new(Vec::new())), } } pub async fn benchmark<F, T>(&self, test_name: &str, iterations: usize, test_fn: F) -> PerformanceMetric where F: Fn() -> T + Send + Sync, T: Send + Sync, { let start_time = std::time::Instant::now(); let start_memory = @memory.get_usage(); let start_cpu = @cpu.get_usage(); for _ in 0..iterations { test_fn(); } let duration = start_time.elapsed(); let end_memory = @memory.get_usage(); let end_cpu = @cpu.get_usage(); let metric = PerformanceMetric { test_name: test_name.to_string(), duration, memory_usage: end_memory - start_memory, cpu_usage: end_cpu - start_cpu, }; self.metrics.lock().await.push(metric.clone()); metric } pub async fn benchmark_async<F, T>(&self, test_name: &str, iterations: usize, test_fn: F) -> PerformanceMetric where F: Fn() -> std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send>> + Send + Sync, T: Send + Sync, { let start_time = std::time::Instant::now(); let start_memory = @memory.get_usage(); let start_cpu = @cpu.get_usage(); let mut futures = Vec::new(); for _ in 0..iterations { futures.push(test_fn()); } futures::future::join_all(futures).await; let duration = start_time.elapsed(); let end_memory = @memory.get_usage(); let end_cpu = @cpu.get_usage(); let metric = PerformanceMetric { test_name: test_name.to_string(), duration, memory_usage: end_memory - start_memory, cpu_usage: end_cpu - start_cpu, }; self.metrics.lock().await.push(metric.clone()); metric } pub async fn generate_report(&self) -> String { let metrics = self.metrics.lock().await; let mut report = String::from("Performance Test Report\n"); report.push_str("=====================\n\n"); for metric in metrics.iter() { report.push_str(&format!("Test: {}\n", metric.test_name)); report.push_str(&format!(" Duration: {:?}\n", metric.duration)); report.push_str(&format!(" Memory Usage: {} bytes\n", metric.memory_usage)); report.push_str(&format!(" CPU Usage: {:.2}%\n", metric.cpu_usage)); report.push_str("\n"); } report } }

// Performance test example #[tokio::test] async fn test_database_performance() { let framework = PerformanceTestFramework::new(); // Benchmark database operations let metric = framework.benchmark_async("database_insert", 1000, || { Box::pin(async { let user = User::new(1, "Test User".to_string(), "test@example.com".to_string()); @db.users.insert(&user).await.unwrap(); }) }).await; // Assert performance requirements assert!(metric.duration.as_millis() < 5000, "Database insert took too long: {:?}", metric.duration); assert!(metric.memory_usage < 10 1024 1024, "Memory usage too high: {} bytes", metric.memory_usage); // Generate and save report let report = framework.generate_report().await; @file.write("performance_report.txt", &report).unwrap(); }

Test Data Generation

// Test data generator
pub struct TestDataGenerator {
    faker: Faker,
}

impl TestDataGenerator { pub fn new() -> Self { Self { faker: Faker::new() } } pub fn generate_user(&self) -> User { User::new( self.faker.number().between(1, 10000), self.faker.name().name(), self.faker.internet().email(), ) } pub fn generate_product(&self) -> Product { Product::new( self.faker.number().between(1, 10000), self.faker.commerce().product_name(), self.faker.commerce().price(), self.faker.commerce().category(), ) } pub fn generate_tusk_config(&self) -> String { format!( r#" app_name = "{}" database_url = "{}" api_key = "@env.get('API_KEY')" debug_mode = {} max_connections = {} "#, self.faker.company().name(), self.faker.internet().url(), self.faker.random().boolean(), self.faker.number().between(10, 100) ) } pub fn generate_test_data_file(&self, count: usize) -> String { let mut data = serde_json::Map::new(); let users: Vec<_> = (0..count).map(|_| { serde_json::to_value(self.generate_user()).unwrap() }).collect(); let products: Vec<_> = (0..count).map(|_| { serde_json::to_value(self.generate_product()).unwrap() }).collect(); data.insert("users".to_string(), serde_json::Value::Array(users)); data.insert("products".to_string(), serde_json::Value::Array(products)); serde_json::to_string_pretty(&serde_json::Value::Object(data)).unwrap() } }

// Usage in tests #[test] fn test_with_generated_data() { let generator = TestDataGenerator::new(); // Generate test users let users: Vec<_> = (0..10).map(|_| generator.generate_user()).collect(); for user in users { assert!(user.is_valid()); assert!(!user.name.is_empty()); assert!(user.email.contains('@')); } // Generate TuskLang config let config_str = generator.generate_tusk_config(); let config = @config.parse(&config_str); assert!(config.is_ok()); }

Test Coverage and Reporting

// Test coverage framework
pub struct TestCoverageFramework {
    coverage_data: std::sync::Arc<tokio::sync::Mutex<CoverageData>>,
}

#[derive(Default)] pub struct CoverageData { pub functions_called: std::collections::HashSet<String>, pub branches_taken: std::collections::HashSet<String>, pub lines_executed: std::collections::HashSet<u32>, }

impl TestCoverageFramework { pub fn new() -> Self { Self { coverage_data: std::sync::Arc::new(tokio::sync::Mutex::new(CoverageData::default())), } } pub async fn record_function_call(&self, function_name: &str) { self.coverage_data.lock().await.functions_called.insert(function_name.to_string()); } pub async fn record_branch_taken(&self, branch_id: &str) { self.coverage_data.lock().await.branches_taken.insert(branch_id.to_string()); } pub async fn record_line_executed(&self, line_number: u32) { self.coverage_data.lock().await.lines_executed.insert(line_number); } pub async fn generate_coverage_report(&self, source_files: &[String]) -> String { let coverage_data = self.coverage_data.lock().await; let mut report = String::from("Test Coverage Report\n"); report.push_str("===================\n\n"); report.push_str(&format!("Functions Called: {}\n", coverage_data.functions_called.len())); report.push_str(&format!("Branches Taken: {}\n", coverage_data.branches_taken.len())); report.push_str(&format!("Lines Executed: {}\n", coverage_data.lines_executed.len())); report.push_str("\nCovered Functions:\n"); for function in &coverage_data.functions_called { report.push_str(&format!(" - {}\n", function)); } report } }

// Coverage test example #[test] fn test_with_coverage() { let coverage = TestCoverageFramework::new(); // Test function with coverage recording let result = { coverage.record_function_call("calculate_total"); coverage.record_line_executed(10); let items = vec![1, 2, 3, 4, 5]; let total: i32 = items.iter().sum(); coverage.record_line_executed(11); total }; assert_eq!(result, 15); // Generate coverage report let report = coverage.generate_coverage_report(&["src/calculator.rs".to_string()]); @file.write("coverage_report.txt", &report).unwrap(); }

This comprehensive guide covers Rust-specific testing patterns, ensuring type safety, performance, and integration with Rust's testing ecosystem while maintaining the power and flexibility of TuskLang's testing capabilities.