1. rust
  2. /testing
  3. /integration-testing

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", &register_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.