title: "" description: "Reference documentation for Navius " category: "Reference" tags: ["documentation", "reference"] last_updated: "April 3, 2025" version: "1.0"

Testing Guidelines

This document outlines testing guidelines for Navius components, with special focus on testing complex features like the Two-Tier Cache implementation.

Table of Contents

General Testing Principles

  1. Test Isolation: Each test should be isolated from others and not depend on external state
  2. Coverage: Aim for high test coverage, but focus on critical paths and edge cases
  3. Test Behavior: Test the behavior of components, not their implementation details
  4. Reliability: Tests should be reliable and not produce flaky results
  5. Performance: Tests should execute quickly to support rapid development
  6. Readability: Tests should be easy to understand and maintain

Test Types

Unit Tests

  • Test individual functions and methods in isolation
  • Mock external dependencies
  • Focus on specific behavior
#![allow(unused)]
fn main() {
#[test]
fn test_cache_key_formatting() {
    let key = format_cache_key("user", "123");
    assert_eq!(key, "user:123");
}
}

Integration Tests

  • Test interactions between components
  • Use real implementations or realistic mocks
  • Focus on component boundaries
#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_cache_with_redis() {
    let redis = MockRedisClient::new();
    let cache = RedisCache::new(redis);
    
    cache.set("test", b"value", None).await.unwrap();
    let result = cache.get("test").await.unwrap();
    
    assert_eq!(result, b"value");
}
}

End-to-End Tests

  • Test the entire system as a whole
  • Use real external dependencies where possible
  • Focus on user scenarios
#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_user_service_with_cache() {
    let app = test_app().await;
    
    // Create a user
    let user_id = app.create_user("[email protected]").await.unwrap();
    
    // First request should hit the database
    let start = Instant::now();
    let user1 = app.get_user(user_id).await.unwrap();
    let first_request_time = start.elapsed();
    
    // Second request should hit the cache
    let start = Instant::now();
    let user2 = app.get_user(user_id).await.unwrap();
    let second_request_time = start.elapsed();
    
    // Verify cache is faster
    assert!(second_request_time < first_request_time);
    
    // Verify data is the same
    assert_eq!(user1, user2);
}
}

Test Structure

Follow the AAA pattern for test structure:

  1. Arrange: Set up the test conditions
  2. Act: Execute the code under test
  3. Assert: Verify the expected outcome
#![allow(unused)]
fn main() {
#[test]
fn test_cache_ttl() {
    // Arrange
    let mock_clock = MockClock::new();
    let cache = InMemoryCache::new_with_clock(100, mock_clock.clone());
    
    // Act - Set a value with TTL
    cache.set("key", b"value", Some(Duration::from_secs(5))).unwrap();
    
    // Assert - Value exists before expiration
    assert_eq!(cache.get("key").unwrap(), b"value");
    
    // Act - Advance time past TTL
    mock_clock.advance(Duration::from_secs(6));
    
    // Assert - Value is gone after expiration
    assert!(cache.get("key").is_err());
}
}

Testing Complex Components

Testing Cache Implementations

Testing caching components requires special attention to:

  1. Cache Hit/Miss Scenarios

    #![allow(unused)]
    fn main() {
    #[tokio::test]
    async fn test_cache_hit_miss() {
        let cache = create_test_cache().await;
        
        // Test cache miss
        let result = cache.get("missing-key").await;
        assert!(result.is_err());
        assert!(matches!(result.unwrap_err(), AppError::NotFound { .. }));
        
        // Set value and test cache hit
        cache.set("test-key", b"value", None).await.unwrap();
        let result = cache.get("test-key").await.unwrap();
        assert_eq!(result, b"value");
    }
    }
  2. TTL Behavior

    #![allow(unused)]
    fn main() {
    #[tokio::test]
    async fn test_cache_ttl() {
        let cache = create_test_cache().await;
        
        // Set with short TTL
        cache.set("expires", b"value", Some(Duration::from_millis(100))).await.unwrap();
        
        // Verify exists
        let result = cache.get("expires").await.unwrap();
        assert_eq!(result, b"value");
        
        // Wait for expiration
        tokio::time::sleep(Duration::from_millis(150)).await;
        
        // Verify expired
        let result = cache.get("expires").await;
        assert!(result.is_err());
    }
    }
  3. Two-Tier Cache Promotion

    #![allow(unused)]
    fn main() {
    #[tokio::test]
    async fn test_two_tier_promotion() {
        let fast_cache = MockCache::new("fast");
        let slow_cache = MockCache::new("slow");
        
        // Configure mocks
        fast_cache.expect_get().with(eq("key")).return_error(AppError::not_found("key"));
        slow_cache.expect_get().with(eq("key")).return_once(|_| Ok(b"value".to_vec()));
        fast_cache.expect_set().with(eq("key"), eq(b"value".to_vec()), any()).return_once(|_, _, _| Ok(()));
        
        let two_tier = TwoTierCache::new(
            Box::new(fast_cache),
            Box::new(slow_cache),
            true, // promote_on_get
            None,
            None,
        );
        
        // Item should be fetched from slow cache and promoted to fast cache
        let result = two_tier.get("key").await.unwrap();
        assert_eq!(result, b"value");
    }
    }
  4. Redis Unavailability

    #![allow(unused)]
    fn main() {
    #[tokio::test]
    async fn test_redis_unavailable() {
        let config = CacheConfig {
            redis_url: "redis://nonexistent:6379",
            // other config...
        };
        
        // Create cache with invalid Redis URL
        let cache = create_memory_only_two_tier_cache(&config, None).await;
        
        // Should still work using just the memory cache
        cache.set("test", b"value", None).await.unwrap();
        let result = cache.get("test").await.unwrap();
        assert_eq!(result, b"value");
    }
    }
  5. Concurrent Operations

    #![allow(unused)]
    fn main() {
    #[tokio::test]
    async fn test_concurrent_operations() {
        let cache = create_test_cache().await;
        
        // Spawn multiple tasks writing to the same key
        let mut handles = vec![];
        for i in 0..10 {
            let cache_clone = cache.clone();
            let handle = tokio::spawn(async move {
                let value = format!("value-{}", i).into_bytes();
                cache_clone.set("concurrent-key", value, None).await.unwrap();
            });
            handles.push(handle);
        }
        
        // Wait for all operations to complete
        for handle in handles {
            handle.await.unwrap();
        }
        
        // Verify key exists
        let result = cache.get("concurrent-key").await;
        assert!(result.is_ok());
    }
    }

Testing Server Customization

For server customization components, focus on:

  1. Feature Flag Combinations
  2. Feature Dependency Resolution
  3. Configuration Validation
  4. Build System Integration

Test Coverage Requirements

Aim for the following coverage levels:

Component TypeMinimum Coverage
Core Services90%
Cache Implementations95%
Utilities80%
API Handlers85%
Configuration90%

Mocking and Test Doubles

  1. Use Mock Implementations for External Dependencies

    #![allow(unused)]
    fn main() {
    #[derive(Clone)]
    struct MockRedisClient {
        data: Arc<RwLock<HashMap<String, Vec<u8>>>>,
    }
    
    #[async_trait]
    impl RedisClient for MockRedisClient {
        async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, RedisError> {
            let data = self.data.read().await;
            Ok(data.get(key).cloned())
        }
        
        async fn set(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> Result<(), RedisError> {
            let mut data = self.data.write().await;
            data.insert(key.to_string(), value);
            Ok(())
        }
        
        // Other methods...
    }
    }
  2. Inject Test Doubles

    #![allow(unused)]
    fn main() {
    #[tokio::test]
    async fn test_cache_with_mock_redis() {
        let redis = MockRedisClient::new();
        let cache = RedisCache::new(Arc::new(redis));
        
        // Test cache operations...
    }
    }
  3. Use Test Fixtures for Common Setup

    #![allow(unused)]
    fn main() {
    async fn create_test_cache() -> Arc<Box<dyn DynCacheOperations>> {
        let config = CacheConfig {
            redis_url: "redis://localhost:6379".to_string(),
            // other test config...
        };
        
        // Use in-memory implementation for tests
        create_memory_only_two_tier_cache(&config, None).await
    }
    }

CI/CD Integration

  1. Run Tests on Every PR

    # In .gitlab-ci.yml
    test:
      stage: test
      script:
        - cargo test
    
  2. Track Code Coverage

    coverage:
      stage: test
      script:
        - cargo install cargo-tarpaulin
        - cargo tarpaulin --out Xml
        - upload-coverage coverage.xml
    
  3. Enforce Coverage Thresholds

    coverage:
      stage: test
      script:
        - cargo tarpaulin --out Xml --fail-under 85
    

Frequently Asked Questions

How to test async code?

Use the tokio::test attribute for async tests:

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_async_cache_operations() {
    let cache = create_test_cache().await;
    // Test async operations...
}
}

How to test error handling?

Test both success and error cases:

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_cache_error_handling() {
    let cache = create_test_cache().await;
    
    // Test missing key
    let result = cache.get("nonexistent").await;
    assert!(result.is_err());
    
    // Test invalid serialization
    let typed_cache = cache.get_typed_cache::<User>();
    let result = typed_cache.get("invalid-json").await;
    assert!(result.is_err());
}
}

How to test with real Redis?

For integration tests, use a real Redis instance:

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_with_real_redis() {
    // Skip if Redis is not available
    if !is_redis_available("redis://localhost:6379").await {
        println!("Skipping test: Redis not available");
        return;
    }
    
    let config = CacheConfig {
        redis_url: "redis://localhost:6379".to_string(),
        // other config...
    };
    
    let cache = create_two_tier_cache(&config, None).await.unwrap();
    
    // Test with real Redis...
}
}