Integration Testing
Integration testing in Rust verifies that different parts of your application work correctly together. Unlike unit tests, integration tests exercise your code as external users would, testing the complete system or significant subsystems.
Integration Test Structure
Basic Integration Test Setup
Integration tests in Rust live in the tests/
directory at the project root, alongside src/
:
my_project/
├── Cargo.toml
├── src/
│ ├── lib.rs
│ └── main.rs
└── tests/
├── integration_test.rs
├── common/
│ └── mod.rs
└── api_tests.rs
Your First Integration Test
tests/integration_test.rs
:
use my_project::Calculator;
#[test]
fn test_calculator_integration() {
let calc = Calculator::new();
// Test a complete workflow
let result = calc.add(10, 5);
let result = calc.multiply(result, 2);
let result = calc.divide(result, 3);
assert_eq!(result, Ok(10));
}
#[test]
fn test_calculator_error_handling() {
let calc = Calculator::new();
// Test error propagation
let result = calc.divide(10, 0);
assert!(result.is_err());
match result {
Err(e) => assert_eq!(e, "Division by zero"),
Ok(_) => panic!("Expected error"),
}
}
Common Test Utilities
tests/common/mod.rs
:
use std::process::{Command, Stdio};
use std::time::Duration;
use tokio::time::sleep;
// Test database setup
pub async fn setup_test_database() -> String {
let db_name = format!("test_db_{}", uuid::Uuid::new_v4());
// Create test database
Command::new("createdb")
.arg(&db_name)
.output()
.expect("Failed to create test database");
format!("postgresql://localhost/{}", db_name)
}
pub async fn cleanup_test_database(db_name: &str) {
let db_name = db_name.split('/').last().unwrap();
Command::new("dropdb")
.arg(db_name)
.output()
.ok(); // Don't panic if cleanup fails
}
// Test server setup
pub struct TestServer {
pub port: u16,
pub base_url: String,
}
impl TestServer {
pub async fn start() -> Self {
let port = find_free_port();
let base_url = format!("http://localhost:{}", port);
// Start server in background
tokio::spawn(async move {
my_project::start_server(port).await
});
// Wait for server to start
sleep(Duration::from_millis(100)).await;
TestServer { port, base_url }
}
}
fn find_free_port() -> u16 {
use std::net::{TcpListener, SocketAddr};
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
addr.port()
}
// Test data builders
pub struct UserBuilder {
name: String,
email: String,
age: Option<u32>,
}
impl UserBuilder {
pub fn new() -> Self {
UserBuilder {
name: "Test User".to_string(),
email: "[email protected]".to_string(),
age: None,
}
}
pub fn name(mut self, name: &str) -> Self {
self.name = name.to_string();
self
}
pub fn email(mut self, email: &str) -> Self {
self.email = email.to_string();
self
}
pub fn age(mut self, age: u32) -> Self {
self.age = Some(age);
self
}
pub fn build(self) -> my_project::User {
my_project::User {
id: uuid::Uuid::new_v4(),
name: self.name,
email: self.email,
age: self.age.unwrap_or(25),
}
}
}
// HTTP client helpers
pub struct TestClient {
client: reqwest::Client,
base_url: String,
}
impl TestClient {
pub fn new(base_url: String) -> Self {
TestClient {
client: reqwest::Client::new(),
base_url,
}
}
pub async fn get(&self, path: &str) -> reqwest::Response {
self.client
.get(&format!("{}{}", self.base_url, path))
.send()
.await
.expect("Failed to send GET request")
}
pub async fn post<T: serde::Serialize>(&self, path: &str, body: &T) -> reqwest::Response {
self.client
.post(&format!("{}{}", self.base_url, path))
.json(body)
.send()
.await
.expect("Failed to send POST request")
}
pub async fn put<T: serde::Serialize>(&self, path: &str, body: &T) -> reqwest::Response {
self.client
.put(&format!("{}{}", self.base_url, path))
.json(body)
.send()
.await
.expect("Failed to send PUT request")
}
pub async fn delete(&self, path: &str) -> reqwest::Response {
self.client
.delete(&format!("{}{}", self.base_url, path))
.send()
.await
.expect("Failed to send DELETE request")
}
}
Database Integration Testing
Testing with Real Databases
tests/database_integration.rs
:
use sqlx::PgPool;
use my_project::{UserRepository, CreateUserRequest};
mod common;
#[tokio::test]
async fn test_user_repository_integration() {
// Setup
let database_url = common::setup_test_database().await;
let pool = PgPool::connect(&database_url).await.unwrap();
// Run migrations
sqlx::migrate!("./migrations").run(&pool).await.unwrap();
let mut repo = UserRepository::new(pool.clone());
// Test create user
let create_request = CreateUserRequest {
name: "Alice Johnson".to_string(),
email: "[email protected]".to_string(),
age: 30,
};
let created_user = repo.create_user(create_request).await.unwrap();
assert_eq!(created_user.name, "Alice Johnson");
assert_eq!(created_user.email, "[email protected]");
assert!(created_user.id.is_some());
// Test get user
let user_id = created_user.id.unwrap();
let retrieved_user = repo.get_user(user_id).await.unwrap();
assert!(retrieved_user.is_some());
assert_eq!(retrieved_user.unwrap().name, "Alice Johnson");
// Test update user
let update_request = UpdateUserRequest {
name: Some("Alice Smith".to_string()),
email: None,
age: Some(31),
};
let updated_user = repo.update_user(user_id, update_request).await.unwrap();
assert!(updated_user.is_some());
assert_eq!(updated_user.unwrap().name, "Alice Smith");
// Test list users
let users = repo.list_users(10, 0).await.unwrap();
assert_eq!(users.len(), 1);
// Test delete user
let deleted = repo.delete_user(user_id).await.unwrap();
assert!(deleted);
let deleted_user = repo.get_user(user_id).await.unwrap();
assert!(deleted_user.is_none());
// Cleanup
common::cleanup_test_database(&database_url).await;
}
#[tokio::test]
async fn test_user_repository_constraints() {
let database_url = common::setup_test_database().await;
let pool = PgPool::connect(&database_url).await.unwrap();
sqlx::migrate!("./migrations").run(&pool).await.unwrap();
let mut repo = UserRepository::new(pool);
// Create first user
let create_request = CreateUserRequest {
name: "Alice".to_string(),
email: "[email protected]".to_string(),
age: 30,
};
repo.create_user(create_request).await.unwrap();
// Try to create user with same email (should fail)
let duplicate_request = CreateUserRequest {
name: "Bob".to_string(),
email: "[email protected]".to_string(), // Same email
age: 25,
};
let result = repo.create_user(duplicate_request).await;
assert!(result.is_err());
common::cleanup_test_database(&database_url).await;
}
#[tokio::test]
async fn test_transaction_rollback() {
let database_url = common::setup_test_database().await;
let pool = PgPool::connect(&database_url).await.unwrap();
sqlx::migrate!("./migrations").run(&pool).await.unwrap();
let repo = UserRepository::new(pool.clone());
// Test transaction that should rollback
let result = sqlx::query!("BEGIN").execute(&pool).await.unwrap();
let create_request = CreateUserRequest {
name: "Alice".to_string(),
email: "[email protected]".to_string(),
age: 30,
};
let user = repo.create_user(create_request).await.unwrap();
// Simulate error and rollback
sqlx::query!("ROLLBACK").execute(&pool).await.unwrap();
// User should not exist after rollback
let retrieved = repo.get_user(user.id.unwrap()).await.unwrap();
assert!(retrieved.is_none());
common::cleanup_test_database(&database_url).await;
}
Using TestContainers for Database Testing
Add to Cargo.toml
:
[dev-dependencies]
testcontainers = "0.14"
tokio = { version = "1", features = ["full"] }
tests/testcontainer_integration.rs
:
use testcontainers::{clients, images, Docker};
use sqlx::PgPool;
use my_project::UserRepository;
#[tokio::test]
async fn test_with_postgres_container() {
let docker = clients::Cli::default();
let postgres_image = images::postgres::Postgres::default();
let node = docker.run(postgres_image);
let connection_string = format!(
"postgres://postgres:[email protected]:{}/postgres",
node.get_host_port_ipv4(5432)
);
let pool = PgPool::connect(&connection_string).await.unwrap();
// Run migrations
sqlx::migrate!("./migrations").run(&pool).await.unwrap();
let repo = UserRepository::new(pool);
// Run tests
let create_request = CreateUserRequest {
name: "Container Test User".to_string(),
email: "[email protected]".to_string(),
age: 25,
};
let user = repo.create_user(create_request).await.unwrap();
assert!(user.id.is_some());
// Container is automatically cleaned up when `node` goes out of scope
}
#[tokio::test]
async fn test_multiple_database_operations() {
let docker = clients::Cli::default();
let postgres_image = images::postgres::Postgres::default();
let node = docker.run(postgres_image);
let connection_string = format!(
"postgres://postgres:[email protected]:{}/postgres",
node.get_host_port_ipv4(5432)
);
let pool = PgPool::connect(&connection_string).await.unwrap();
sqlx::migrate!("./migrations").run(&pool).await.unwrap();
let repo = UserRepository::new(pool);
// Create multiple users
let users = vec![
CreateUserRequest {
name: "Alice".to_string(),
email: "[email protected]".to_string(),
age: 30,
},
CreateUserRequest {
name: "Bob".to_string(),
email: "[email protected]".to_string(),
age: 25,
},
CreateUserRequest {
name: "Charlie".to_string(),
email: "[email protected]".to_string(),
age: 35,
},
];
let mut created_users = Vec::new();
for user_req in users {
let user = repo.create_user(user_req).await.unwrap();
created_users.push(user);
}
// Test listing users
let all_users = repo.list_users(10, 0).await.unwrap();
assert_eq!(all_users.len(), 3);
// Test search functionality
let alice_users = repo.search_users("Alice", 10, 0).await.unwrap();
assert_eq!(alice_users.len(), 1);
assert_eq!(alice_users[0].name, "Alice");
// Test pagination
let first_page = repo.list_users(2, 0).await.unwrap();
let second_page = repo.list_users(2, 2).await.unwrap();
assert_eq!(first_page.len(), 2);
assert_eq!(second_page.len(), 1);
// Ensure no overlap
let first_ids: Vec<_> = first_page.iter().map(|u| u.id).collect();
let second_ids: Vec<_> = second_page.iter().map(|u| u.id).collect();
for id in &second_ids {
assert!(!first_ids.contains(id));
}
}
API Integration Testing
Testing REST APIs
tests/api_integration.rs
:
use serde_json::json;
use my_project::{CreateUserRequest, User};
mod common;
#[tokio::test]
async fn test_user_api_crud() {
// Start test server
let test_server = common::TestServer::start().await;
let client = common::TestClient::new(test_server.base_url);
// Test POST /users (create)
let create_request = CreateUserRequest {
name: "API Test User".to_string(),
email: "[email protected]".to_string(),
age: 28,
};
let response = client.post("/users", &create_request).await;
assert_eq!(response.status(), 201);
let created_user: User = response.json().await.unwrap();
assert_eq!(created_user.name, "API Test User");
assert!(created_user.id.is_some());
let user_id = created_user.id.unwrap();
// Test GET /users/{id} (read)
let response = client.get(&format!("/users/{}", user_id)).await;
assert_eq!(response.status(), 200);
let retrieved_user: User = response.json().await.unwrap();
assert_eq!(retrieved_user.id, Some(user_id));
assert_eq!(retrieved_user.name, "API Test User");
// Test PUT /users/{id} (update)
let update_request = json!({
"name": "Updated API User",
"age": 29
});
let response = client.put(&format!("/users/{}", user_id), &update_request).await;
assert_eq!(response.status(), 200);
let updated_user: User = response.json().await.unwrap();
assert_eq!(updated_user.name, "Updated API User");
assert_eq!(updated_user.age, 29);
// Test GET /users (list)
let response = client.get("/users").await;
assert_eq!(response.status(), 200);
let users: Vec<User> = response.json().await.unwrap();
assert!(!users.is_empty());
assert!(users.iter().any(|u| u.id == Some(user_id)));
// Test DELETE /users/{id}
let response = client.delete(&format!("/users/{}", user_id)).await;
assert_eq!(response.status(), 204);
// Verify user is deleted
let response = client.get(&format!("/users/{}", user_id)).await;
assert_eq!(response.status(), 404);
}
#[tokio::test]
async fn test_api_error_handling() {
let test_server = common::TestServer::start().await;
let client = common::TestClient::new(test_server.base_url);
// Test invalid request body
let invalid_request = json!({
"name": "", // Empty name should be invalid
"email": "invalid-email", // Invalid email format
});
let response = client.post("/users", &invalid_request).await;
assert_eq!(response.status(), 400);
let error_response: serde_json::Value = response.json().await.unwrap();
assert!(error_response["errors"].is_array());
// Test non-existent resource
let response = client.get("/users/00000000-0000-0000-0000-000000000000").await;
assert_eq!(response.status(), 404);
// Test method not allowed
let response = client.client
.patch(&format!("{}/users", test_server.base_url))
.send()
.await
.unwrap();
assert_eq!(response.status(), 405);
}
#[tokio::test]
async fn test_api_pagination() {
let test_server = common::TestServer::start().await;
let client = common::TestClient::new(test_server.base_url);
// Create multiple users for pagination testing
for i in 0..15 {
let create_request = CreateUserRequest {
name: format!("User {}", i),
email: format!("user{}@example.com", i),
age: 20 + i,
};
let response = client.post("/users", &create_request).await;
assert_eq!(response.status(), 201);
}
// Test default pagination
let response = client.get("/users").await;
assert_eq!(response.status(), 200);
let users: Vec<User> = response.json().await.unwrap();
assert!(users.len() <= 10); // Default page size
// Test explicit pagination
let response = client.get("/users?limit=5&offset=0").await;
assert_eq!(response.status(), 200);
let first_page: Vec<User> = response.json().await.unwrap();
assert_eq!(first_page.len(), 5);
let response = client.get("/users?limit=5&offset=5").await;
assert_eq!(response.status(), 200);
let second_page: Vec<User> = response.json().await.unwrap();
assert_eq!(second_page.len(), 5);
// Verify pages don't overlap
let first_ids: Vec<_> = first_page.iter().map(|u| u.id).collect();
let second_ids: Vec<_> = second_page.iter().map(|u| u.id).collect();
for id in &second_ids {
assert!(!first_ids.contains(id));
}
// Test search with pagination
let response = client.get("/users?search=User&limit=3").await;
assert_eq!(response.status(), 200);
let search_results: Vec<User> = response.json().await.unwrap();
assert_eq!(search_results.len(), 3);
assert!(search_results.iter().all(|u| u.name.contains("User")));
}
#[tokio::test]
async fn test_concurrent_api_requests() {
let test_server = common::TestServer::start().await;
let client = common::TestClient::new(test_server.base_url);
use tokio::task::JoinSet;
let mut join_set = JoinSet::new();
// Create 10 users concurrently
for i in 0..10 {
let client = client.clone();
join_set.spawn(async move {
let create_request = CreateUserRequest {
name: format!("Concurrent User {}", i),
email: format!("concurrent{}@example.com", i),
age: 20 + i,
};
client.post("/users", &create_request).await
});
}
let mut successful_creations = 0;
while let Some(result) = join_set.join_next().await {
let response = result.unwrap();
if response.status() == 201 {
successful_creations += 1;
}
}
assert_eq!(successful_creations, 10);
// Verify all users were created
let response = client.get("/users").await;
let users: Vec<User> = response.json().await.unwrap();
let concurrent_users: Vec<_> = users.iter()
.filter(|u| u.name.starts_with("Concurrent User"))
.collect();
assert_eq!(concurrent_users.len(), 10);
}
Testing GraphQL APIs
tests/graphql_integration.rs
:
use serde_json::json;
mod common;
#[tokio::test]
async fn test_graphql_user_queries() {
let test_server = common::TestServer::start().await;
let client = common::TestClient::new(test_server.base_url);
// Create a user first via REST API for testing
let create_request = CreateUserRequest {
name: "GraphQL Test User".to_string(),
email: "[email protected]".to_string(),
age: 30,
};
let response = client.post("/users", &create_request).await;
let created_user: User = response.json().await.unwrap();
let user_id = created_user.id.unwrap();
// Test GraphQL query
let query = json!({
"query": format!(r#"
query {{
user(id: "{}") {{
id
name
email
age
}}
}}
"#, user_id)
});
let response = client.post("/graphql", &query).await;
assert_eq!(response.status(), 200);
let graphql_response: serde_json::Value = response.json().await.unwrap();
assert!(graphql_response["errors"].is_null());
let user_data = &graphql_response["data"]["user"];
assert_eq!(user_data["name"], "GraphQL Test User");
assert_eq!(user_data["email"], "[email protected]");
assert_eq!(user_data["age"], 30);
}
#[tokio::test]
async fn test_graphql_user_mutations() {
let test_server = common::TestServer::start().await;
let client = common::TestClient::new(test_server.base_url);
// Test create user mutation
let mutation = json!({
"query": r#"
mutation {
createUser(input: {
name: "GraphQL Created User"
email: "[email protected]"
age: 25
}) {
id
name
email
age
}
}
"#
});
let response = client.post("/graphql", &mutation).await;
assert_eq!(response.status(), 200);
let graphql_response: serde_json::Value = response.json().await.unwrap();
assert!(graphql_response["errors"].is_null());
let created_user = &graphql_response["data"]["createUser"];
let user_id = created_user["id"].as_str().unwrap();
assert_eq!(created_user["name"], "GraphQL Created User");
// Test update user mutation
let update_mutation = json!({
"query": format!(r#"
mutation {{
updateUser(id: "{}", input: {{
name: "Updated via GraphQL"
age: 26
}}) {{
id
name
age
}}
}}
"#, user_id)
});
let response = client.post("/graphql", &update_mutation).await;
assert_eq!(response.status(), 200);
let graphql_response: serde_json::Value = response.json().await.unwrap();
assert!(graphql_response["errors"].is_null());
let updated_user = &graphql_response["data"]["updateUser"];
assert_eq!(updated_user["name"], "Updated via GraphQL");
assert_eq!(updated_user["age"], 26);
}
#[tokio::test]
async fn test_graphql_complex_queries() {
let test_server = common::TestServer::start().await;
let client = common::TestClient::new(test_server.base_url);
// Create users with posts for complex query testing
let user_mutation = json!({
"query": r#"
mutation {
createUser(input: {
name: "Author User"
email: "[email protected]"
age: 35
}) {
id
}
}
"#
});
let response = client.post("/graphql", &user_mutation).await;
let user_response: serde_json::Value = response.json().await.unwrap();
let author_id = user_response["data"]["createUser"]["id"].as_str().unwrap();
// Create posts for the user
for i in 0..3 {
let post_mutation = json!({
"query": format!(r#"
mutation {{
createPost(input: {{
title: "Post {}"
content: "Content for post {}"
authorId: "{}"
}}) {{
id
}}
}}
"#, i, i, author_id)
});
client.post("/graphql", &post_mutation).await;
}
// Test complex query with nested data
let complex_query = json!({
"query": format!(r#"
query {{
user(id: "{}") {{
id
name
email
posts {{
id
title
content
createdAt
}}
postCount
}}
}}
"#, author_id)
});
let response = client.post("/graphql", &complex_query).await;
assert_eq!(response.status(), 200);
let graphql_response: serde_json::Value = response.json().await.unwrap();
assert!(graphql_response["errors"].is_null());
let user_data = &graphql_response["data"]["user"];
assert_eq!(user_data["name"], "Author User");
assert_eq!(user_data["postCount"], 3);
let posts = user_data["posts"].as_array().unwrap();
assert_eq!(posts.len(), 3);
for (i, post) in posts.iter().enumerate() {
assert_eq!(post["title"], format!("Post {}", i));
assert!(post["createdAt"].is_string());
}
}
Testing External Services
HTTP Service Integration
tests/external_service_integration.rs
:
use wiremock::{MockServer, Mock, ResponseTemplate};
use wiremock::matchers::{method, path, body_json};
use serde_json::json;
use my_project::{ExternalApiClient, WeatherService};
mod common;
#[tokio::test]
async fn test_external_api_integration() {
// Start mock server
let mock_server = MockServer::start().await;
// Setup mock response
Mock::given(method("GET"))
.and(path("/weather"))
.respond_with(ResponseTemplate::new(200)
.set_body_json(json!({
"temperature": 22.5,
"humidity": 65,
"condition": "sunny"
})))
.mount(&mock_server)
.await;
// Test our service with mock
let api_client = ExternalApiClient::new(&mock_server.uri());
let weather_service = WeatherService::new(api_client);
let weather = weather_service.get_current_weather("New York").await.unwrap();
assert_eq!(weather.temperature, 22.5);
assert_eq!(weather.humidity, 65);
assert_eq!(weather.condition, "sunny");
}
#[tokio::test]
async fn test_external_api_error_handling() {
let mock_server = MockServer::start().await;
// Setup mock error response
Mock::given(method("GET"))
.and(path("/weather"))
.respond_with(ResponseTemplate::new(500)
.set_body_json(json!({
"error": "Internal server error"
})))
.mount(&mock_server)
.await;
let api_client = ExternalApiClient::new(&mock_server.uri());
let weather_service = WeatherService::new(api_client);
let result = weather_service.get_current_weather("Invalid City").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_external_api_timeout() {
let mock_server = MockServer::start().await;
// Setup slow response
Mock::given(method("GET"))
.and(path("/weather"))
.respond_with(ResponseTemplate::new(200)
.set_delay(std::time::Duration::from_secs(10)) // Longer than client timeout
.set_body_json(json!({"temperature": 20.0})))
.mount(&mock_server)
.await;
let api_client = ExternalApiClient::new(&mock_server.uri())
.with_timeout(std::time::Duration::from_secs(1));
let weather_service = WeatherService::new(api_client);
let result = weather_service.get_current_weather("New York").await;
assert!(result.is_err());
// Verify it's a timeout error
match result.unwrap_err() {
my_project::WeatherError::Timeout => {}, // Expected
other => panic!("Expected timeout error, got: {:?}", other),
}
}
#[tokio::test]
async fn test_external_api_authentication() {
let mock_server = MockServer::start().await;
// Setup mock that requires authentication
Mock::given(method("GET"))
.and(path("/protected/data"))
.and(wiremock::matchers::header("Authorization", "Bearer valid_token"))
.respond_with(ResponseTemplate::new(200)
.set_body_json(json!({"data": "secret_data"})))
.mount(&mock_server)
.await;
// Setup mock for invalid auth
Mock::given(method("GET"))
.and(path("/protected/data"))
.respond_with(ResponseTemplate::new(401)
.set_body_json(json!({"error": "Unauthorized"})))
.mount(&mock_server)
.await;
let api_client = ExternalApiClient::new(&mock_server.uri())
.with_bearer_token("valid_token");
let result = api_client.get_protected_data().await.unwrap();
assert_eq!(result.data, "secret_data");
// Test with invalid token
let api_client_invalid = ExternalApiClient::new(&mock_server.uri())
.with_bearer_token("invalid_token");
let result = api_client_invalid.get_protected_data().await;
assert!(result.is_err());
}
Message Queue Integration
tests/message_queue_integration.rs
:
use testcontainers::{clients, images};
use tokio::time::{sleep, Duration};
use my_project::{MessagePublisher, MessageConsumer, UserCreatedEvent};
mod common;
#[tokio::test]
async fn test_rabbitmq_integration() {
let docker = clients::Cli::default();
let rabbitmq_image = images::rabbitmq::RabbitMq::default();
let node = docker.run(rabbitmq_image);
let amqp_url = format!(
"amqp://127.0.0.1:{}",
node.get_host_port_ipv4(5672)
);
// Wait for RabbitMQ to start
sleep(Duration::from_secs(5)).await;
// Setup publisher and consumer
let publisher = MessagePublisher::connect(&amqp_url).await.unwrap();
let consumer = MessageConsumer::connect(&amqp_url).await.unwrap();
// Test message publishing and consumption
let event = UserCreatedEvent {
user_id: uuid::Uuid::new_v4(),
email: "[email protected]".to_string(),
created_at: chrono::Utc::now(),
};
// Publish message
publisher.publish_user_created(&event).await.unwrap();
// Consume message
let received_events = consumer.consume_user_created_events(1).await.unwrap();
assert_eq!(received_events.len(), 1);
let received_event = &received_events[0];
assert_eq!(received_event.user_id, event.user_id);
assert_eq!(received_event.email, event.email);
}
#[tokio::test]
async fn test_message_queue_error_handling() {
let docker = clients::Cli::default();
let rabbitmq_image = images::rabbitmq::RabbitMq::default();
let node = docker.run(rabbitmq_image);
let amqp_url = format!(
"amqp://127.0.0.1:{}",
node.get_host_port_ipv4(5672)
);
sleep(Duration::from_secs(5)).await;
let publisher = MessagePublisher::connect(&amqp_url).await.unwrap();
let consumer = MessageConsumer::connect(&amqp_url).await.unwrap();
// Test publishing invalid message
let invalid_event = "not a valid event";
let result = publisher.publish_raw(invalid_event).await;
assert!(result.is_err());
// Test consumer timeout
let result = consumer
.consume_user_created_events_with_timeout(1, Duration::from_millis(100))
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_message_queue_ordering() {
let docker = clients::Cli::default();
let rabbitmq_image = images::rabbitmq::RabbitMq::default();
let node = docker.run(rabbitmq_image);
let amqp_url = format!(
"amqp://127.0.0.1:{}",
node.get_host_port_ipv4(5672)
);
sleep(Duration::from_secs(5)).await;
let publisher = MessagePublisher::connect(&amqp_url).await.unwrap();
let consumer = MessageConsumer::connect(&amqp_url).await.unwrap();
// Publish multiple events in order
let mut events = Vec::new();
for i in 0..5 {
let event = UserCreatedEvent {
user_id: uuid::Uuid::new_v4(),
email: format!("user{}@example.com", i),
created_at: chrono::Utc::now(),
};
publisher.publish_user_created(&event).await.unwrap();
events.push(event);
// Small delay to ensure ordering
sleep(Duration::from_millis(10)).await;
}
// Consume all events
let received_events = consumer.consume_user_created_events(5).await.unwrap();
assert_eq!(received_events.len(), 5);
// Verify ordering
for (i, received_event) in received_events.iter().enumerate() {
assert_eq!(received_event.email, format!("user{}@example.com", i));
}
}
End-to-End Testing
Full Application Workflow Tests
tests/e2e_workflow.rs
:
use serde_json::json;
use my_project::{CreateUserRequest, CreatePostRequest};
mod common;
#[tokio::test]
async fn test_complete_user_journey() {
// Setup test environment
let database_url = common::setup_test_database().await;
let test_server = common::TestServer::start_with_database(&database_url).await;
let client = common::TestClient::new(test_server.base_url);
// 1. User Registration
let register_request = CreateUserRequest {
name: "Journey Test User".to_string(),
email: "[email protected]".to_string(),
age: 28,
};
let response = client.post("/users", ®ister_request).await;
assert_eq!(response.status(), 201);
let user: User = response.json().await.unwrap();
let user_id = user.id.unwrap();
// 2. User Login (if authentication is implemented)
let login_request = json!({
"email": "[email protected]",
"password": "password123"
});
let response = client.post("/auth/login", &login_request).await;
assert_eq!(response.status(), 200);
let auth_response: AuthResponse = response.json().await.unwrap();
let token = auth_response.token;
// 3. Create authenticated client
let authed_client = common::TestClient::new(test_server.base_url.clone())
.with_bearer_token(&token);
// 4. Create Posts
let mut post_ids = Vec::new();
for i in 0..3 {
let create_post_request = CreatePostRequest {
title: format!("Journey Post {}", i),
content: format!("This is the content for post {}", i),
tags: vec![format!("tag{}", i), "journey".to_string()],
};
let response = authed_client.post("/posts", &create_post_request).await;
assert_eq!(response.status(), 201);
let post: Post = response.json().await.unwrap();
post_ids.push(post.id.unwrap());
}
// 5. Get User's Posts
let response = authed_client.get(&format!("/users/{}/posts", user_id)).await;
assert_eq!(response.status(), 200);
let posts: Vec<Post> = response.json().await.unwrap();
assert_eq!(posts.len(), 3);
// 6. Update a Post
let update_request = json!({
"title": "Updated Journey Post",
"content": "This post has been updated during the journey test"
});
let response = authed_client.put(&format!("/posts/{}", post_ids[0]), &update_request).await;
assert_eq!(response.status(), 200);
let updated_post: Post = response.json().await.unwrap();
assert_eq!(updated_post.title, "Updated Journey Post");
// 7. Comment on Posts
for post_id in &post_ids {
let comment_request = json!({
"content": format!("Comment on post {}", post_id)
});
let response = authed_client.post(&format!("/posts/{}/comments", post_id), &comment_request).await;
assert_eq!(response.status(), 201);
}
// 8. Get Posts with Comments
let response = client.get(&format!("/posts/{}/comments", post_ids[0])).await;
assert_eq!(response.status(), 200);
let comments: Vec<Comment> = response.json().await.unwrap();
assert_eq!(comments.len(), 1);
// 9. Search Posts
let response = client.get("/posts/search?q=Journey").await;
assert_eq!(response.status(), 200);
let search_results: Vec<Post> = response.json().await.unwrap();
assert!(search_results.len() >= 2); // At least 2 posts match "Journey"
// 10. User Profile Update
let profile_update = json!({
"name": "Updated Journey User",
"age": 29
});
let response = authed_client.put(&format!("/users/{}", user_id), &profile_update).await;
assert_eq!(response.status(), 200);
// 11. Cleanup - Delete Posts and User
for post_id in post_ids {
let response = authed_client.delete(&format!("/posts/{}", post_id)).await;
assert_eq!(response.status(), 204);
}
let response = authed_client.delete(&format!("/users/{}", user_id)).await;
assert_eq!(response.status(), 204);
// Cleanup database
common::cleanup_test_database(&database_url).await;
}
#[tokio::test]
async fn test_error_scenarios_workflow() {
let database_url = common::setup_test_database().await;
let test_server = common::TestServer::start_with_database(&database_url).await;
let client = common::TestClient::new(test_server.base_url);
// 1. Try to access protected resource without authentication
let response = client.get("/users/me").await;
assert_eq!(response.status(), 401);
// 2. Try to create user with invalid data
let invalid_user = json!({
"name": "",
"email": "not-an-email",
"age": -5
});
let response = client.post("/users", &invalid_user).await;
assert_eq!(response.status(), 400);
// 3. Try to access non-existent resource
let response = client.get("/users/00000000-0000-0000-0000-000000000000").await;
assert_eq!(response.status(), 404);
// 4. Create user and try duplicate registration
let user_request = CreateUserRequest {
name: "Error Test User".to_string(),
email: "[email protected]".to_string(),
age: 25,
};
let response = client.post("/users", &user_request).await;
assert_eq!(response.status(), 201);
// Try to create same user again
let response = client.post("/users", &user_request).await;
assert_eq!(response.status(), 409); // Conflict
// 5. Test rate limiting (if implemented)
for _ in 0..100 {
client.get("/users").await;
}
let response = client.get("/users").await;
// Should hit rate limit eventually
if response.status() == 429 {
println!("Rate limiting is working");
}
common::cleanup_test_database(&database_url).await;
}
Performance Integration Testing
Load Testing
tests/performance_integration.rs
:
use std::time::{Duration, Instant};
use tokio::task::JoinSet;
use my_project::CreateUserRequest;
mod common;
#[tokio::test]
async fn test_concurrent_user_creation() {
let database_url = common::setup_test_database().await;
let test_server = common::TestServer::start_with_database(&database_url).await;
let base_client = common::TestClient::new(test_server.base_url);
const CONCURRENT_REQUESTS: usize = 50;
const TIMEOUT: Duration = Duration::from_secs(30);
let start_time = Instant::now();
let mut join_set = JoinSet::new();
// Spawn concurrent user creation requests
for i in 0..CONCURRENT_REQUESTS {
let client = base_client.clone();
join_set.spawn(async move {
let user_request = CreateUserRequest {
name: format!("Load Test User {}", i),
email: format!("loadtest{}@example.com", i),
age: 20 + (i % 50) as u32,
};
let request_start = Instant::now();
let response = client.post("/users", &user_request).await;
let request_duration = request_start.elapsed();
(response.status(), request_duration)
});
}
let mut successful_requests = 0;
let mut failed_requests = 0;
let mut total_response_time = Duration::ZERO;
let mut max_response_time = Duration::ZERO;
let mut min_response_time = Duration::MAX;
// Collect results
while let Some(result) = join_set.join_next().await {
let (status, duration) = result.unwrap();
if status.is_success() {
successful_requests += 1;
} else {
failed_requests += 1;
}
total_response_time += duration;
max_response_time = max_response_time.max(duration);
min_response_time = min_response_time.min(duration);
}
let total_duration = start_time.elapsed();
let average_response_time = total_response_time / CONCURRENT_REQUESTS as u32;
let requests_per_second = CONCURRENT_REQUESTS as f64 / total_duration.as_secs_f64();
println!("Performance Test Results:");
println!(" Total requests: {}", CONCURRENT_REQUESTS);
println!(" Successful: {}", successful_requests);
println!(" Failed: {}", failed_requests);
println!(" Total time: {:?}", total_duration);
println!(" Average response time: {:?}", average_response_time);
println!(" Min response time: {:?}", min_response_time);
println!(" Max response time: {:?}", max_response_time);
println!(" Requests per second: {:.2}", requests_per_second);
// Assertions for performance requirements
assert!(total_duration < TIMEOUT, "Test took too long: {:?}", total_duration);
assert!(successful_requests >= CONCURRENT_REQUESTS * 95 / 100, "Too many failed requests");
assert!(average_response_time < Duration::from_millis(500), "Average response time too high");
assert!(requests_per_second > 10.0, "Throughput too low");
common::cleanup_test_database(&database_url).await;
}
#[tokio::test]
async fn test_database_connection_pool_stress() {
let database_url = common::setup_test_database().await;
let test_server = common::TestServer::start_with_database(&database_url).await;
let client = common::TestClient::new(test_server.base_url);
const PARALLEL_STREAMS: usize = 10;
const REQUESTS_PER_STREAM: usize = 20;
let mut join_set = JoinSet::new();
for stream_id in 0..PARALLEL_STREAMS {
let client = client.clone();
join_set.spawn(async move {
let mut stream_errors = 0;
for request_id in 0..REQUESTS_PER_STREAM {
let user_request = CreateUserRequest {
name: format!("Stream {} User {}", stream_id, request_id),
email: format!("stream{}user{}@example.com", stream_id, request_id),
age: 25,
};
let response = client.post("/users", &user_request).await;
if !response.status().is_success() {
stream_errors += 1;
}
// Small delay to simulate realistic usage
tokio::time::sleep(Duration::from_millis(10)).await;
}
stream_errors
});
}
let mut total_errors = 0;
while let Some(result) = join_set.join_next().await {
total_errors += result.unwrap();
}
let total_requests = PARALLEL_STREAMS * REQUESTS_PER_STREAM;
let error_rate = total_errors as f64 / total_requests as f64;
println!("Connection Pool Stress Test:");
println!(" Total requests: {}", total_requests);
println!(" Total errors: {}", total_errors);
println!(" Error rate: {:.2}%", error_rate * 100.0);
assert!(error_rate < 0.05, "Error rate too high: {:.2}%", error_rate * 100.0);
common::cleanup_test_database(&database_url).await;
}
Integration testing ensures your Rust application works correctly as a whole. Focus on testing realistic user scenarios, external service interactions, and system boundaries. Use tools like TestContainers for reliable test environments, and always clean up resources after tests complete.