Integration Testing Rust with Docker: From Theory to Practice
Modern Rust development requires not only checking individual functions (unit tests), but also testing the interaction of components with external services — databases, caches, APIs. Integration testing with Docker allows you to isolate the environment, guarantee reproducibility, and avoid problems with "it works on my machine". In this article, we will analyze how to set up integration tests for Rust applications using Docker.
Why Docker for Rust Integration Testing?
Docker solves three key problems:
- Environment isolation — tests run in a clean container, without the influence of local settings
- Reproducibility — identical conditions on all developer machines and in CI/CD
- Dependency management — automatic startup of PostgreSQL, Redis, Kafka and other services
Project Setup: Structure and Dependencies
Let's create a project with integration tests for an Actix-web web server that works with PostgreSQL.
# Cargo.toml[package]name = "rust-docker-testing"version = "0.1.0"edition = "2021"
[dependencies]actix-web = "4"serde = { version = "1", features = ["derive"] }tokio = { version = "1", features = ["full"] }sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }dotenv = "0.15"
[dev-dependencies]testcontainers = "0.14"reqwest = { version = "0.11", features = ["json"] }Note testcontainers — this is a Rust library for managing Docker containers directly from tests.
Project Structure
Recommended structure for integration tests:
.├── src/│ ├── main.rs│ ├── lib.rs│ ├── models.rs│ └── handlers.rs├── tests/│ ├── common/│ │ └── mod.rs # Common utilities for tests│ ├── integration_tests.rs # Main integration tests│ └── docker-compose.yml # For manual startup├── Dockerfile└── .envStep 1: Writing an Integration Test with Docker
Let's create a test that starts PostgreSQL in Docker, runs migrations, and checks the API.
// tests/integration_tests.rsuse testcontainers::clients;use testcontainers::images::postgres::Postgres;use sqlx::postgres::PgPoolOptions;use std::net::TcpListener;
async fn setup_test_db() -> (String, testcontainers::Container) { let docker = clients::Cli::default(); let postgres_image = Postgres::default(); let container = docker.run(postgres_image); let host = container.get_host().unwrap(); let port = container.get_host_port_ipv4(5432).unwrap(); let database_url = format!("postgres://postgres:postgres@{}:{}/postgres", host, port); // Execute migrations let pool = PgPoolOptions::new() .max_connections(5) .connect(&database_url) .await .expect("Failed to connect to Postgres"); sqlx::query("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL)") .execute(&pool) .await .expect("Migration failed"); (database_url, container)}
#[tokio::test]async fn test_create_user() { let (db_url, _container) = setup_test_db().await; // Start the application on a random port let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let port = listener.local_addr().unwrap().port(); let server = actix_web::HttpServer::new(move || { actix_web::App::new() .app_data(actix_web::web::Data::new(db_url.clone())) .route("/users", actix_web::web::post().to(handlers::create_user)) }) .listen(listener) .unwrap() .run(); tokio::spawn(server); // Wait for the server to start tokio::ti