🦀 Testing Framework in TuskLang for Rust
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.