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
- Test Types
- Test Structure
- Testing Complex Components
- Test Coverage Requirements
- Mocking and Test Doubles
- CI/CD Integration
General Testing Principles
- Test Isolation: Each test should be isolated from others and not depend on external state
- Coverage: Aim for high test coverage, but focus on critical paths and edge cases
- Test Behavior: Test the behavior of components, not their implementation details
- Reliability: Tests should be reliable and not produce flaky results
- Performance: Tests should execute quickly to support rapid development
- 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:
- Arrange: Set up the test conditions
- Act: Execute the code under test
- 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:
-
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"); } }
-
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()); } }
-
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"); } }
-
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"); } }
-
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:
- Feature Flag Combinations
- Feature Dependency Resolution
- Configuration Validation
- Build System Integration
Test Coverage Requirements
Aim for the following coverage levels:
Component Type | Minimum Coverage |
---|---|
Core Services | 90% |
Cache Implementations | 95% |
Utilities | 80% |
API Handlers | 85% |
Configuration | 90% |
Mocking and Test Doubles
-
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... } }
-
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... } }
-
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
-
Run Tests on Every PR
# In .gitlab-ci.yml test: stage: test script: - cargo test
-
Track Code Coverage
coverage: stage: test script: - cargo install cargo-tarpaulin - cargo tarpaulin --out Xml - upload-coverage coverage.xml
-
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... } }