Testing
This guide covers testing strategies, tools, and best practices for BWS development and deployment.
Testing Overview
BWS includes multiple layers of testing:
- Unit Tests: Test individual functions and modules
- Integration Tests: Test component interactions
- End-to-End Tests: Test complete workflows
- Performance Tests: Measure performance characteristics
- Security Tests: Validate security measures
Running Tests
Basic Test Commands
# Run all tests
cargo test
# Run tests with output
cargo test -- --nocapture
# Run specific test
cargo test test_config_parsing
# Run tests matching pattern
cargo test config
# Run ignored tests
cargo test -- --ignored
# Run tests in single thread (for debugging)
cargo test -- --test-threads=1
Test Categories
# Run only unit tests
cargo test --lib
# Run only integration tests
cargo test --test integration
# Run only documentation tests
cargo test --doc
# Run tests for specific package
cargo test -p bws-core
Test with Features
# Test with all features
cargo test --all-features
# Test with specific features
cargo test --features "compression,metrics"
# Test without default features
cargo test --no-default-features
Unit Testing
Basic Unit Tests
#![allow(unused)] fn main() { // src/config.rs #[cfg(test)] mod tests { use super::*; #[test] fn test_config_default() { let config = Config::default(); assert_eq!(config.sites.len(), 0); } #[test] fn test_config_parsing() { let toml_str = r#" [[sites]] name = "test" hostname = "localhost" port = 8080 static_dir = "static" "#; let config: Config = toml::from_str(toml_str).unwrap(); assert_eq!(config.sites.len(), 1); assert_eq!(config.sites[0].name, "test"); assert_eq!(config.sites[0].port, 8080); } #[test] #[should_panic(expected = "Invalid port")] fn test_invalid_port() { let site = Site { name: "test".to_string(), hostname: "localhost".to_string(), port: 0, // Invalid port static_dir: "static".to_string(), ..Default::default() }; site.validate().unwrap(); } } }
Testing Error Conditions
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use anyhow::Result; #[test] fn test_file_not_found() { let result = read_config_file("nonexistent.toml"); assert!(result.is_err()); let error = result.unwrap_err(); assert!(error.to_string().contains("No such file")); } #[test] fn test_invalid_toml() { let invalid_toml = "invalid toml content [[["; let result = parse_config(invalid_toml); assert!(result.is_err()); } #[test] fn test_missing_required_field() { let toml_str = r#" [[sites]] name = "test" Missing hostname, port, static_dir "#; let result: Result<Config, _> = toml::from_str(toml_str); assert!(result.is_err()); } } }
Mocking and Test Doubles
#![allow(unused)] fn main() { // Use mockall for mocking use mockall::predicate::*; use mockall::mock; mock! { FileSystem { fn read_file(&self, path: &str) -> Result<String>; fn file_exists(&self, path: &str) -> bool; } } #[cfg(test)] mod tests { use super::*; #[test] fn test_config_loading_with_mock() { let mut mock_fs = MockFileSystem::new(); mock_fs .expect_read_file() .with(eq("config.toml")) .times(1) .returning(|_| Ok(r#" [[sites]] name = "test" hostname = "localhost" port = 8080 static_dir = "static" "#.to_string())); let config = load_config_with_fs(&mock_fs, "config.toml").unwrap(); assert_eq!(config.sites.len(), 1); } } }
Integration Testing
Test Structure
#![allow(unused)] fn main() { // tests/integration/server_tests.rs use bws::{Config, Server}; use std::time::Duration; use tokio::time::sleep; #[tokio::test] async fn test_server_startup_shutdown() { let config = test_config(); let server = Server::new(config).await.unwrap(); // Start server in background let handle = tokio::spawn(async move { server.run().await }); // Give server time to start sleep(Duration::from_millis(100)).await; // Test server is responding let response = reqwest::get("http://127.0.0.1:8080/health").await.unwrap(); assert_eq!(response.status(), 200); // Shutdown server handle.abort(); } #[tokio::test] async fn test_static_file_serving() { let temp_dir = setup_test_static_files().await; let config = Config { sites: vec![Site { name: "test".to_string(), hostname: "127.0.0.1".to_string(), port: 8081, static_dir: temp_dir.path().to_string_lossy().to_string(), ..Default::default() }], ..Default::default() }; let server = Server::new(config).await.unwrap(); let handle = tokio::spawn(async move { server.run().await }); sleep(Duration::from_millis(100)).await; // Test serving static file let response = reqwest::get("http://127.0.0.1:8081/test.html").await.unwrap(); assert_eq!(response.status(), 200); assert_eq!(response.text().await.unwrap(), "<h1>Test</h1>"); handle.abort(); cleanup_test_files(temp_dir).await; } fn test_config() -> Config { Config { sites: vec![Site { name: "test".to_string(), hostname: "127.0.0.1".to_string(), port: 8080, static_dir: "test_static".to_string(), ..Default::default() }], ..Default::default() } } async fn setup_test_static_files() -> tempfile::TempDir { let temp_dir = tempfile::tempdir().unwrap(); tokio::fs::write( temp_dir.path().join("test.html"), "<h1>Test</h1>" ).await.unwrap(); tokio::fs::write( temp_dir.path().join("index.html"), "<h1>Index</h1>" ).await.unwrap(); temp_dir } }
HTTP Client Testing
#![allow(unused)] fn main() { // tests/integration/http_tests.rs use reqwest::Client; use serde_json::Value; #[tokio::test] async fn test_health_endpoint() { let client = Client::new(); let response = client .get("http://127.0.0.1:8080/health") .send() .await .unwrap(); assert_eq!(response.status(), 200); assert_eq!(response.headers().get("content-type").unwrap(), "application/json"); let body: Value = response.json().await.unwrap(); assert_eq!(body["status"], "healthy"); } #[tokio::test] async fn test_custom_headers() { let client = Client::new(); let response = client .get("http://127.0.0.1:8080/") .send() .await .unwrap(); // Check custom headers are present assert!(response.headers().contains_key("x-served-by")); assert_eq!(response.headers()["cache-control"], "public, max-age=3600"); } #[tokio::test] async fn test_cors_headers() { let client = Client::new(); let response = client .options("http://127.0.0.1:8080/") .header("Origin", "https://example.com") .header("Access-Control-Request-Method", "GET") .send() .await .unwrap(); assert_eq!(response.status(), 200); assert!(response.headers().contains_key("access-control-allow-origin")); } }
Database Integration Tests
#![allow(unused)] fn main() { // tests/integration/database_tests.rs (if BWS had database features) use sqlx::PgPool; #[tokio::test] async fn test_database_connection() { let pool = setup_test_database().await; let config = Config { database_url: Some(pool.connect_options().to_url_lossy().to_string()), ..test_config() }; let server = Server::new(config).await.unwrap(); // Test database-dependent endpoints let response = reqwest::get("http://127.0.0.1:8080/api/data").await.unwrap(); assert_eq!(response.status(), 200); cleanup_test_database(pool).await; } async fn setup_test_database() -> PgPool { // Set up test database PgPool::connect("postgres://test:test@localhost/bws_test") .await .unwrap() } }
End-to-End Testing
Test Scenarios
#![allow(unused)] fn main() { // tests/e2e/scenarios.rs use std::process::{Command, Stdio}; use std::time::Duration; use tokio::time::sleep; #[tokio::test] async fn test_complete_deployment_scenario() { // 1. Create test configuration let config_content = r#" [daemon] pid_file = "/tmp/bws-test.pid" [logging] level = "info" output = "file" file_path = "/tmp/bws-test.log" [[sites]] name = "main" hostname = "127.0.0.1" port = 8080 static_dir = "test_static" [sites.headers] "Cache-Control" = "public, max-age=3600" "#; std::fs::write("test-config.toml", config_content).unwrap(); // 2. Create static files std::fs::create_dir_all("test_static").unwrap(); std::fs::write("test_static/index.html", "<h1>Welcome to BWS</h1>").unwrap(); std::fs::write("test_static/style.css", "body { color: blue; }").unwrap(); // 3. Start BWS server let mut child = Command::new("target/release/bws") .arg("--config") .arg("test-config.toml") .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .unwrap(); // 4. Wait for server to start sleep(Duration::from_secs(2)).await; // 5. Run tests test_homepage().await; test_static_files().await; test_health_check().await; test_performance().await; // 6. Cleanup child.kill().unwrap(); std::fs::remove_file("test-config.toml").unwrap(); std::fs::remove_dir_all("test_static").unwrap(); std::fs::remove_file("/tmp/bws-test.log").ok(); std::fs::remove_file("/tmp/bws-test.pid").ok(); } async fn test_homepage() { let response = reqwest::get("http://127.0.0.1:8080/").await.unwrap(); assert_eq!(response.status(), 200); assert!(response.text().await.unwrap().contains("Welcome to BWS")); } async fn test_static_files() { let response = reqwest::get("http://127.0.0.1:8080/style.css").await.unwrap(); assert_eq!(response.status(), 200); assert_eq!(response.headers()["content-type"], "text/css"); assert!(response.text().await.unwrap().contains("color: blue")); } async fn test_health_check() { let response = reqwest::get("http://127.0.0.1:8080/health").await.unwrap(); assert_eq!(response.status(), 200); let health: serde_json::Value = response.json().await.unwrap(); assert_eq!(health["status"], "healthy"); } async fn test_performance() { use std::time::Instant; let start = Instant::now(); // Make 100 concurrent requests let futures: Vec<_> = (0..100) .map(|_| reqwest::get("http://127.0.0.1:8080/")) .collect(); let responses = futures::future::join_all(futures).await; let duration = start.elapsed(); // All requests should succeed for response in responses { assert_eq!(response.unwrap().status(), 200); } // Should complete in reasonable time assert!(duration < Duration::from_secs(5)); println!("100 requests completed in {:?}", duration); } }
Multi-Site Testing
#![allow(unused)] fn main() { #[tokio::test] async fn test_multi_site_configuration() { let config_content = r#" [[sites]] name = "main" hostname = "127.0.0.1" port = 8080 static_dir = "main_static" [[sites]] name = "api" hostname = "127.0.0.1" port = 8081 static_dir = "api_static" [sites.headers] "Content-Type" = "application/json" "#; // Setup and test both sites setup_multi_site_files(); let mut child = start_bws_server("multi-site-config.toml"); sleep(Duration::from_secs(2)).await; // Test main site let response = reqwest::get("http://127.0.0.1:8080/").await.unwrap(); assert_eq!(response.status(), 200); // Test API site let response = reqwest::get("http://127.0.0.1:8081/").await.unwrap(); assert_eq!(response.status(), 200); assert_eq!(response.headers()["content-type"], "application/json"); cleanup_multi_site_test(child); } }
WebSocket Proxy Testing
#![allow(unused)] fn main() { // tests/integration/websocket_tests.rs use tokio_tungstenite::{connect_async, tungstenite::Message}; #[tokio::test] async fn test_websocket_proxy_configuration() { let config_content = r#" [[sites]] name = "websocket-proxy" hostname = "127.0.0.1" port = 8090 static_dir = "static" [sites.proxy] enabled = true [[sites.proxy.upstreams]] name = "websocket_backend" url = "http://127.0.0.1:3001" weight = 1 [[sites.proxy.upstreams]] name = "websocket_backend" url = "http://127.0.0.1:3002" weight = 1 [[sites.proxy.routes]] path = "/ws" upstream = "websocket_backend" strip_prefix = true websocket = true [sites.proxy.load_balancing] method = "round_robin" "#; // Start mock WebSocket servers let server1 = start_mock_websocket_server(3001).await; let server2 = start_mock_websocket_server(3002).await; // Start BWS with WebSocket proxy config let bws_server = start_bws_server_with_config(config_content).await; // Test WebSocket upgrade detection test_websocket_upgrade_detection().await; // Test WebSocket proxy connection test_websocket_proxy_connection().await; // Test load balancing test_websocket_load_balancing().await; // Cleanup cleanup_websocket_test(bws_server, server1, server2).await; } async fn test_websocket_upgrade_detection() { // Test that BWS properly detects WebSocket upgrade requests let client = reqwest::Client::new(); let response = client .get("http://127.0.0.1:8090/ws") .header("Upgrade", "websocket") .header("Connection", "Upgrade") .header("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==") .header("Sec-WebSocket-Version", "13") .send() .await .unwrap(); // Should attempt WebSocket upgrade (not 404) assert_ne!(response.status(), 404); } async fn test_websocket_proxy_connection() { // Note: This test demonstrates the framework // Full implementation requires additional Pingora integration // Attempt WebSocket connection through proxy let ws_url = "ws://127.0.0.1:8090/ws"; // In a complete implementation, this would succeed match connect_async(ws_url).await { Ok((mut ws_stream, _)) => { // Send test message ws_stream.send(Message::Text("test".to_string())).await.unwrap(); // Receive response if let Some(msg) = ws_stream.next().await { let msg = msg.unwrap(); assert!(msg.is_text()); println!("Received: {}", msg.to_text().unwrap()); } } Err(e) => { // Expected in current implementation println!("WebSocket connection failed (expected): {}", e); } } } async fn test_websocket_load_balancing() { // Test that WebSocket connections are load balanced let mut server_responses = std::collections::HashMap::new(); // Make multiple connections for i in 0..10 { let ws_url = format!("ws://127.0.0.1:8090/ws?test={}", i); // In full implementation, track which server responds // Current implementation provides detection framework match connect_async(&ws_url).await { Ok((mut ws_stream, _)) => { ws_stream.send(Message::Text("ping".to_string())).await.unwrap(); if let Some(msg) = ws_stream.next().await { let response = msg.unwrap().to_text().unwrap(); *server_responses.entry(response.to_string()).or_insert(0) += 1; } } Err(_) => { // Expected in current framework implementation } } } // In full implementation, verify load balancing distribution println!("Server response distribution: {:?}", server_responses); } async fn start_mock_websocket_server(port: u16) -> tokio::task::JoinHandle<()> { tokio::spawn(async move { use tokio_tungstenite::{accept_async, tungstenite::Message}; use tokio::net::{TcpListener, TcpStream}; let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).await.unwrap(); println!("Mock WebSocket server listening on port {}", port); while let Ok((stream, _)) = listener.accept().await { tokio::spawn(handle_websocket_connection(stream, port)); } }) } async fn handle_websocket_connection(stream: TcpStream, port: u16) { match accept_async(stream).await { Ok(mut websocket) => { while let Some(msg) = websocket.next().await { match msg { Ok(Message::Text(text)) => { let response = format!("Echo from server {}: {}", port, text); websocket.send(Message::Text(response)).await.unwrap(); } Ok(Message::Close(_)) => break, Err(e) => { println!("WebSocket error: {}", e); break; } _ => {} } } } Err(e) => println!("WebSocket handshake failed: {}", e), } } }
WebSocket Test Script
#!/bin/bash
# Run the WebSocket proxy test script
./tests/test_websocket_proxy.sh
This script will:
- Start multiple WebSocket test servers
- Configure BWS with WebSocket proxy routes
- Provide a web interface for manual testing
- Demonstrate load balancing between upstream servers
Performance Testing
Load Testing with wrk
#!/bin/bash
# scripts/load-test.sh
BWS_PID=""
setup_test_server() {
echo "Setting up test server..."
# Create test configuration
cat > test-load-config.toml << EOF
[[sites]]
name = "load-test"
hostname = "127.0.0.1"
port = 8080
static_dir = "load_test_static"
[sites.headers]
"Cache-Control" = "public, max-age=3600"
EOF
# Create test files
mkdir -p load_test_static
echo "<h1>Load Test Page</h1>" > load_test_static/index.html
# Generate test files of various sizes
dd if=/dev/zero of=load_test_static/1kb.txt bs=1024 count=1 2>/dev/null
dd if=/dev/zero of=load_test_static/10kb.txt bs=1024 count=10 2>/dev/null
dd if=/dev/zero of=load_test_static/100kb.txt bs=1024 count=100 2>/dev/null
# Start BWS
./target/release/bws --config test-load-config.toml &
BWS_PID=$!
sleep 2
}
run_load_tests() {
echo "Running load tests..."
# Test 1: Basic load test
echo "=== Basic Load Test ==="
wrk -t4 -c50 -d30s --latency http://127.0.0.1:8080/
# Test 2: High concurrency
echo "=== High Concurrency Test ==="
wrk -t8 -c200 -d30s --latency http://127.0.0.1:8080/
# Test 3: Different file sizes
echo "=== 1KB File Test ==="
wrk -t4 -c50 -d15s http://127.0.0.1:8080/1kb.txt
echo "=== 10KB File Test ==="
wrk -t4 -c50 -d15s http://127.0.0.1:8080/10kb.txt
echo "=== 100KB File Test ==="
wrk -t4 -c50 -d15s http://127.0.0.1:8080/100kb.txt
# Test 4: Sustained load
echo "=== Sustained Load Test (5 minutes) ==="
wrk -t4 -c100 -d300s --latency http://127.0.0.1:8080/
}
cleanup_test() {
echo "Cleaning up..."
if [ ! -z "$BWS_PID" ]; then
kill $BWS_PID 2>/dev/null
fi
rm -rf load_test_static test-load-config.toml
}
# Trap cleanup on script exit
trap cleanup_test EXIT
setup_test_server
run_load_tests
Benchmark Tests
#![allow(unused)] fn main() { // benches/server_benchmark.rs use criterion::{black_box, criterion_group, criterion_main, Criterion}; use bws::{Config, Server}; fn bench_config_parsing(c: &mut Criterion) { let config_str = r#" [[sites]] name = "bench" hostname = "127.0.0.1" port = 8080 static_dir = "static" [sites.headers] "Cache-Control" = "public, max-age=3600" "X-Content-Type-Options" = "nosniff" "#; c.bench_function("parse config", |b| { b.iter(|| { let _config: Config = toml::from_str(black_box(config_str)).unwrap(); }) }); } fn bench_static_file_resolution(c: &mut Criterion) { let config = test_config(); c.bench_function("resolve static file", |b| { b.iter(|| { let _path = resolve_static_file( black_box("/assets/css/main.css"), black_box(&config.sites[0]) ); }) }); } criterion_group!(benches, bench_config_parsing, bench_static_file_resolution); criterion_main!(benches); }
Memory Testing
#![allow(unused)] fn main() { // tests/memory_tests.rs #[test] fn test_memory_usage() { use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; let memory_counter = Arc::new(AtomicUsize::new(0)); // Custom allocator to track memory usage #[global_allocator] static GLOBAL: TrackingAllocator = TrackingAllocator; let initial_memory = get_memory_usage(); // Create large number of configs let configs: Vec<Config> = (0..1000) .map(|i| Config { sites: vec![Site { name: format!("site_{}", i), hostname: "127.0.0.1".to_string(), port: 8080 + i, static_dir: format!("static_{}", i), ..Default::default() }], ..Default::default() }) .collect(); let peak_memory = get_memory_usage(); drop(configs); // Force garbage collection std::hint::black_box(()); let final_memory = get_memory_usage(); println!("Initial memory: {} KB", initial_memory / 1024); println!("Peak memory: {} KB", peak_memory / 1024); println!("Final memory: {} KB", final_memory / 1024); // Memory should be released assert!(final_memory < peak_memory); } }
Security Testing
Input Validation Tests
#![allow(unused)] fn main() { #[tokio::test] async fn test_path_traversal_protection() { // Test various path traversal attempts let malicious_paths = vec![ "../../../etc/passwd", "..%2F..%2F..%2Fetc%2Fpasswd", "....//....//....//etc//passwd", "%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd", ]; for path in malicious_paths { let response = reqwest::get(&format!("http://127.0.0.1:8080/{}", path)) .await .unwrap(); // Should return 404 or 403, not 200 assert_ne!(response.status(), 200); } } #[tokio::test] async fn test_request_size_limits() { let client = reqwest::Client::new(); // Test large request body let large_body = "x".repeat(10 * 1024 * 1024); // 10MB let response = client .post("http://127.0.0.1:8080/") .body(large_body) .send() .await .unwrap(); // Should reject large requests assert_eq!(response.status(), 413); // Payload Too Large } #[tokio::test] async fn test_header_injection() { let client = reqwest::Client::new(); // Test header injection attempts let response = client .get("http://127.0.0.1:8080/") .header("X-Forwarded-For", "malicious\r\nContent-Type: text/html") .send() .await .unwrap(); // Response should not contain injected header assert!(!response.headers().contains_key("content-type")); } }
Rate Limiting Tests
#![allow(unused)] fn main() { #[tokio::test] async fn test_rate_limiting() { let client = reqwest::Client::new(); // Make requests rapidly let mut success_count = 0; let mut rate_limited_count = 0; for _ in 0..100 { let response = client .get("http://127.0.0.1:8080/") .send() .await .unwrap(); match response.status().as_u16() { 200 => success_count += 1, 429 => rate_limited_count += 1, // Too Many Requests _ => {} } } // Should have some rate limited responses assert!(rate_limited_count > 0); println!("Success: {}, Rate Limited: {}", success_count, rate_limited_count); } }
Test Utilities and Helpers
Test Configuration Factory
#![allow(unused)] fn main() { // tests/common/mod.rs pub fn test_config() -> Config { Config { daemon: DaemonConfig::default(), logging: LoggingConfig { level: "debug".to_string(), output: "stdout".to_string(), ..Default::default() }, sites: vec![Site { name: "test".to_string(), hostname: "127.0.0.1".to_string(), port: 8080, static_dir: "test_static".to_string(), ..Default::default() }], ..Default::default() } } pub fn test_config_with_port(port: u16) -> Config { let mut config = test_config(); config.sites[0].port = port; config } pub fn test_config_multi_site() -> Config { Config { sites: vec![ Site { name: "main".to_string(), hostname: "127.0.0.1".to_string(), port: 8080, static_dir: "main_static".to_string(), ..Default::default() }, Site { name: "api".to_string(), hostname: "127.0.0.1".to_string(), port: 8081, static_dir: "api_static".to_string(), ..Default::default() }, ], ..Default::default() } } }
Test Server Management
#![allow(unused)] fn main() { use std::sync::Once; use tokio::sync::Mutex; static INIT: Once = Once::new(); static TEST_SERVER: Mutex<Option<TestServer>> = Mutex::const_new(None); pub struct TestServer { pub port: u16, handle: tokio::task::JoinHandle<()>, } impl TestServer { pub async fn start(config: Config) -> Self { let port = config.sites[0].port; let server = Server::new(config).await.unwrap(); let handle = tokio::spawn(async move { server.run().await.unwrap(); }); // Wait for server to start tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; TestServer { port, handle } } pub fn url(&self) -> String { format!("http://127.0.0.1:{}", self.port) } } impl Drop for TestServer { fn drop(&mut self) { self.handle.abort(); } } // Global test server for shared tests pub async fn get_test_server() -> &'static TestServer { let mut server = TEST_SERVER.lock().await; if server.is_none() { *server = Some(TestServer::start(test_config()).await); } server.as_ref().unwrap() } }
Test File Management
#![allow(unused)] fn main() { use tempfile::{TempDir, NamedTempFile}; pub struct TestStaticFiles { pub temp_dir: TempDir, pub index_file: PathBuf, pub css_file: PathBuf, pub js_file: PathBuf, } impl TestStaticFiles { pub async fn new() -> Self { let temp_dir = TempDir::new().unwrap(); let index_file = temp_dir.path().join("index.html"); tokio::fs::write(&index_file, "<h1>Test Index</h1>").await.unwrap(); let css_file = temp_dir.path().join("style.css"); tokio::fs::write(&css_file, "body { color: red; }").await.unwrap(); let js_file = temp_dir.path().join("script.js"); tokio::fs::write(&js_file, "console.log('test');").await.unwrap(); TestStaticFiles { temp_dir, index_file, css_file, js_file, } } pub fn path(&self) -> &Path { self.temp_dir.path() } } }
Continuous Integration Testing
GitHub Actions Test Workflow
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
name: Test Suite
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
rust: [stable, beta]
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
components: rustfmt, clippy
- name: Cache dependencies
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Check formatting
run: cargo fmt --all -- --check
- name: Run clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Run tests
run: cargo test --all-features --verbose
- name: Run integration tests
run: cargo test --test integration --all-features
- name: Run benchmarks (check only)
run: cargo bench --no-run
Test Coverage
# Add to GitHub Actions
- name: Install coverage tools
run: |
cargo install cargo-tarpaulin
- name: Generate test coverage
run: |
cargo tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./cobertura.xml
Testing Best Practices
Test Organization
- Group related tests in modules
- Use descriptive test names
- Follow AAA pattern (Arrange, Act, Assert)
- Test both happy path and error cases
- Use test fixtures for common setup
Test Data Management
- Use temporary directories for file operations
- Clean up resources in test teardown
- Use factories for creating test objects
- Avoid hardcoded values, use constants
Performance Testing
- Run performance tests in isolated environment
- Use consistent hardware for benchmarks
- Monitor for performance regressions
- Set reasonable performance thresholds
Security Testing
- Test all input validation
- Check authentication and authorization
- Verify secure defaults
- Test rate limiting and DOS protection
Test Maintenance
- Keep tests up to date with code changes
- Remove or update obsolete tests
- Refactor duplicated test code
- Document complex test scenarios
Next Steps
- Set up Continuous Integration
- Learn about Performance Tuning for optimization
- Review Contributing Guidelines for development workflow
- Check Troubleshooting for common issues