Integration Testing Rust with Docker: Advanced Level

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Integration Testing Rust with Docker: Advanced Level



Integration testing is a critical stage in Rust development, especially when your application interacts with external services: databases, message queues, HTTP APIs. Using Docker allows you to create isolated, reproducible environments for tests. In this article, we will explore advanced techniques: from basic setup to parallel test execution using testcontainers and docker-compose.



Why Docker for Integration Tests?

Traditional mocks and stubs often do not reflect the real behavior of dependencies. Docker solves this problem:

  • Isolation: each test runs in a clean container.
  • Reproducibility: the same environment on all developer machines and in CI.
  • Parallelism: you can run multiple test databases simultaneously.


1. Basic Setup: Testing with PostgreSQL

Let's start simple: test a function that works with PostgreSQL. Use the testcontainers crate — a Rust wrapper for managing Docker containers.

Add dependencies to Cargo.toml:

[dev-dependencies]testcontainers = "0.18"tokio = { version = "1", features = ["full"] }sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }


Example test:

use sqlx::PgPool;use testcontainers::clients::Cli;use testcontainers::images::postgres::Postgres;

#[tokio::test]async fn test_database_connection() { let docker = Cli::default(); let postgres_instance = docker.run(Postgres::default()); let connection_string = &format!( "postgres://postgres:postgres@127.0.0.1:{}/postgres", postgres_instance.get_host_port_ipv4(5432) );

let pool = PgPool::connect(connection_string).await.unwrap();

// Execute SQL query let row: (i32,) = sqlx::query_as("SELECT 1 + 1") .fetch_one(&pool) .await .unwrap(); assert_eq!(row.0, 2);}

Important: the container is automatically removed after the test completes (thanks to the Drop implementation).



2. Advanced Configuration: Custom Image with Migrations

Often you need to run migrations beforehand. Let's create a custom image based on a Dockerfile:

# Dockerfile.testFROM postgres:16-alpineCOPY migrations/ /docker-entrypoint-initdb.d/


In the Rust test, use Image:

use testcontainers::images::generic::GenericImage;

#[tokio::test]async fn test_with_migrations() { let docker = Cli::default(); let image = GenericImage::new("my-app-test:latest") .with_wait_for(testcontainers::core::WaitFor::message_on_stdout( "database system is ready to accept connections" )); let instance = docker.run(image); // ... use instance}

To build the image in CI, you can use docker build -f Dockerfile.test -t my-app-test:latest .



3. Parallel Test Execution with Docker Compose

If your service requires multiple dependencies (PostgreSQL + Redis), use docker-compose. Create a file docker-compose.test.yml:

version: '3.8'services:  postgres:    image: postgres:16-alpine    environment:      POSTGRES_PASSWORD: testpass    ports:      - "5432"  redis:    image: redis:7-alpine    ports:      - "6379"


In the Rust test, spin up the Compose environment:

use testcontainers::clients::Cli;use testcontainers::images::compose::Compose;

#[tokio::test]async fn test_with_compose() { let docker = Cli::default(); let compose = Compose::new("docker-compose.test.y

Blogs

Book Recommendations