Integration Testing Rust with Docker: A Complete Guide

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: From A to Z

\n\n

Integration testing is a critically important stage in developing reliable Rust applications. Unlike unit tests, integration tests verify the interaction of components with external dependencies: databases, caches, message queues. Docker is ideal for creating isolated, reproducible test environments. In this article, we will break down how to set up integration testing for a Rust project using Docker, starting from the basic architecture and ending with CI/CD.

\n\n

1. Why Docker is the Best Choice for Rust Integration Tests

\n\n

Rust compiles to native code, but its applications often depend on external services. Docker solves three key problems:

\n
    \n
  • Isolation: each test runs in a clean container, eliminating the influence of previous runs.
  • \n
  • Reproducibility: exact versions of PostgreSQL, Redis, or Kafka guarantee identical behavior on the local machine and in CI.
  • \n
  • Speed: Docker images are cached, and Rust tests are compiled once.
  • \n
\n\n

Let's consider a typical project: a web server on Actix-web, working with PostgreSQL. Our goal is to write a test that spins up a container with the database, runs migrations, and checks CRUD operations.

\n\n

2. Setting Up Test Infrastructure with Docker Compose

\n\n

For convenience, we use docker-compose.yml, which describes all dependencies. Let's create a file in the project root:

\n\n
version: '3.8'\nservices:\n  postgres_test:\n    image: postgres:15-alpine\n    environment:\n      POSTGRES_USER: test_user\n      POSTGRES_PASSWORD: test_pass\n      POSTGRES_DB: test_db\n    ports:\n      - "5433:5432"\n    tmpfs: /var/lib/postgresql/data  # data in memory for speed
\n\n

Note the tmpfs — this speeds up tests since data is not written to disk. Port 5433 is chosen to avoid conflicts with the local database.

\n\n

Now let's write a Rust function for managing containers. We use the testcontainers crate — it allows programmatically launching Docker containers:

\n\n
// tests/common/mod.rs\nuse testcontainers::{clients, images::postgres::Postgres, Container};\n\npub struct TestDb {\n    pub container: Container<'static, Postgres>,\n    pub connection_string: String,\n}\n\nimpl TestDb {\n    pub fn new() -> Self {\n        let docker = clients::Cli::default();\n        let postgres_image = Postgres::default();\n        let container = docker.run(postgres_image);\n        \n        let host = container.get_host();\n        let port = container.get_host_port_ipv4(5432);\n        let connection_string = format!(\n            "postgres://postgres:postgres@{}:{}/postgres",\n            host, port\n        );\n        \n        TestDb { container, connection_string }\n    }\n}
\n\n

This module can be reused in all integration tests. The container will be automatically removed when tests fail or the process completes.

\n\n

3. Writing Integration Tests with Real Requests

\n\n

Let's create a test that checks user insertion and retrieval. Suppose we have a User struct and create_user and get_user functions in the main code.

\n\n
// tests/integration_test.rs\nmod common;\n\nuse sqlx::PgPool;\nuse your_crate::{create_user, get_user, User};\n\n#[tokio::test]\nasync fn test_create_and_get_user() {\n    let test_db = common::TestDb::new();\n    let pool = PgPool::connect(&test_db.connection_string)\n        .await\n        .expect("Failed to connect to DB");\n    \n    // Run migrations (if using sqlx)\n    sqlx::migrate!().run(&pool).await.unwrap();\n    \n    // Create a user\n    let user = User {\n        name: "Alice".into(),\n        email: "alice@example.com".into(),\n    };\n    let created = create_user(&pool, &us                

Blogs

Book Recommendations