Testing Guide
Comprehensive testing utilities for Armature framework applications.
Table of Contents
- Overview
- Features
- Integration Test Helpers
- Docker Test Containers
- Load Testing
- Contract Testing
- Basic Test Utilities
- Best Practices
- Summary
Overview
The armature-testing crate provides a comprehensive suite of testing utilities:
- Integration Helpers - Database setup/teardown
- Docker Containers - Isolated test environments
- Load Testing - Performance and stress testing
- Contract Testing - Consumer-driven contracts (Pact)
- Test App - HTTP test client
- Mocks - Service mocking and spies
- Assertions - Fluent test assertions
Features
โ Integration Test Helpers
- Database setup/teardown automation
- Test fixtures with lifecycle management
- Database seeding utilities
- Migration support
โ Docker Test Containers
- Automatic container lifecycle
- Built-in configurations for Postgres, Redis, MongoDB
- Custom container support
- Auto-cleanup on drop
โ Load Testing
- Request count-based testing
- Duration-based testing
- Concurrent load generation
- Stress testing (gradual ramp-up)
- Detailed statistics (RPS, latency percentiles)
โ Contract Testing
- Pact-compatible contracts
- Consumer-driven design
- Contract versioning
- Verification utilities
Integration Test Helpers
Database Setup/Teardown
use armature_testing::integration::*;
use async_trait::async_trait;
struct MyDbHelper {
connection_string: String,
}
#[async_trait]
impl DatabaseTestHelper for MyDbHelper {
async fn setup(&self) -> Result<(), IntegrationTestError> {
// Connect to database
// Run migrations
// Seed test data
Ok(())
}
async fn teardown(&self) -> Result<(), IntegrationTestError> {
// Drop tables
// Clean up test data
Ok(())
}
}
Test Fixtures
use std::sync::Arc;
let helper = Arc::new(MyDbHelper::new("postgres://localhost/test"));
let fixture = TestFixture::new(helper);
// Automatic setup and teardown
fixture.run_test(|| async {
// Your test code
// Database is ready to use
Ok(())
}).await?;
Docker Test Containers
PostgreSQL Container
use armature_testing::docker::*;
let config = PostgresContainer::config("testdb", "user", "pass");
let mut container = DockerContainer::new(config);
container.start().await?;
// Connection: postgres://user:pass@localhost:5432/testdb
container.stop().await?;
// Or let it drop for auto-cleanup
Redis Container
let config = RedisContainer::config();
let mut container = DockerContainer::new(config);
container.start().await?;
// Connection: redis://localhost:6379
MongoDB Container
let config = MongoContainer::config("testdb");
let mut container = DockerContainer::new(config);
container.start().await?;
// Connection: mongodb://localhost:27017/testdb
Load Testing
Basic Load Test
use armature_testing::load::*;
use std::time::Duration;
let config = LoadTestConfig::new(10, 1000); // 10 concurrent, 1000 requests
let runner = LoadTestRunner::new(config, || async {
// Your test code (e.g., HTTP request)
Ok(())
});
let stats = runner.run().await?;
stats.print();
Duration-Based Load Test
let config = LoadTestConfig::new(20, u64::MAX)
.with_duration(Duration::from_secs(60)) // Run for 60 seconds
.with_timeout(Duration::from_secs(10));
let runner = LoadTestRunner::new(config, || async {
Ok(())
});
let stats = runner.run().await?;
Stress Test (Gradual Ramp-Up)
let stress_runner = StressTestRunner::new(
1, // Start with 1 concurrent
100, // Max 100 concurrent
10, // Step by 10
Duration::from_secs(10), // 10 seconds per step
|| async {
Ok(())
},
);
let results = stress_runner.run().await?;
for (concurrency, stats) in results {
println!("Concurrency {}: {} RPS", concurrency, stats.rps);
}
Load Test Statistics
The LoadTestStats struct provides:
total_requests- Total number of requestssuccessful- Successful requestsfailed- Failed requestsduration- Total test durationrps- Requests per secondmin_response_time- Minimum latencymax_response_time- Maximum latencyavg_response_time- Average latencymedian_response_time- Median (p50)p95_response_time- 95th percentilep99_response_time- 99th percentile
Contract Testing
Creating a Contract
use armature_testing::contract::*;
let mut builder = ContractBuilder::new("Frontend", "UserAPI");
// Define interaction
let request = ContractRequest::new(ContractMethod::Get, "/api/users/1")
.with_header("Accept", "application/json");
let response = ContractResponse::new(200)
.with_header("Content-Type", "application/json")
.with_body(serde_json::json!({
"id": 1,
"name": "Alice"
}));
builder.add_interaction(
ContractInteraction::new(
"get user by ID",
request,
response,
)
.with_provider_state("user with ID 1 exists")
);
let contract = builder.build();
Saving Contracts
use std::path::PathBuf;
let manager = ContractManager::new(PathBuf::from("./pacts"));
manager.save(&contract)?;
// Saves to: ./pacts/frontend-userapi.json
Verifying Contracts
let actual_response = ContractResponse::new(200)
.with_body(serde_json::json!({"id": 1, "name": "Alice"}));
match ContractVerifier::verify_interaction(&interaction, &actual_response) {
Ok(()) => println!("โ
Contract verified"),
Err(e) => println!("โ Verification failed: {}", e),
}
Basic Test Utilities
Test App
use armature_testing::*;
let app = TestAppBuilder::new()
.with_route("/hello", |_req| async {
Ok(HttpResponse::ok().with_body(b"Hello!".to_vec()))
})
.build();
let client = app.client();
let response = client.get("/hello").await;
assert_eq!(response.status(), Some(200));
Mock Services
use armature_testing::MockService;
let mock = MockService::<String>::new();
mock.record_call("get_user");
assert_eq!(mock.call_count(), 1);
assert!(mock.was_called("get_user"));
Assertions
use armature_testing::*;
// Assert status
assert_status(&response, 200);
// Assert header
assert_header(&response, "Content-Type", "application/json");
// Assert JSON
assert_json(&response, &serde_json::json!({"status": "ok"}));
Best Practices
Integration Testing
- Use Fixtures - Automate setup/teardown
- Isolate Tests - Each test should be independent
- Clean Up - Always clean up test data
- Use Transactions - Rollback after each test
- Seed Minimal Data - Only what's needed for the test
Docker Containers
- Check Availability - Always check if Docker is available
- Use RAII - Let containers auto-cleanup
- Wait for Ready - Use wait timeouts
- Unique Names - Use UUIDs for container names
Load Testing
- Start Small - Begin with low concurrency
- Gradual Increase - Use stress tests to find limits
- Monitor Metrics - Track p95/p99, not just average
- Realistic Tests - Use production-like data
Contract Testing
- Consumer-Driven - Let consumers define contracts
- Version Contracts - Track contract versions
- Share Contracts - Use shared repository
- Verify Often - Run verification in CI
Summary
The armature-testing crate provides comprehensive testing utilities:
- โ Integration Helpers - Automate database setup/teardown
- โ Docker Containers - Isolated, reproducible environments
- โ Load Testing - Find performance limits
- โ Contract Testing - Consumer-driven API design
- โ Test Utilities - Mock, assert, test clients
Key Benefits:
- Productivity - Less boilerplate, more testing
- Reliability - Isolated, reproducible tests
- Performance - Find bottlenecks early
- Confidence - Comprehensive test coverage
Happy Testing! ๐งช