testcontainers-rs

repository·main·Indexed 22 days ago

https://github.com/testcontainers/testcontainers-rs

The official Rust port of the Testcontainers project, providing a library for integration-testing against Docker containers. It includes support for both synchronous (SyncRunner) and asynchronous (AsyncRunner) APIs, custom image building via GenericBuildableImage, and configuration options for Docker hosts, private registries, and Podman compatibility.

Tokens
25.8K
Snippets
72
Records
105
Agent score
77%

What's inside testcontainers-rs

  1. Introduction to Testcontainers for Rust

    main
    Testcontainers for Rust is a library designed to simplify the creation and cleanup of container-based dependencies for automated integration and smoke tests. It provides a programmatic API to define containers that run during your tests and ensures those resources are automatically cleaned up once the tests complete.
  2. Understand the purpose of the testimages directory

    main
    The testimages directory contains the build contexts for Docker images used specifically for testing the testcontainers-rs library. By co-locating these build contexts within the repository, the project can create small, lightweight, and specialized images tailored to specific test scenarios, reducing reliance on external dependencies.
  3. Design principles of testcontainers-rs

    main

    The testcontainers-rs library is built around three core design principles to ensure a high-quality developer experience:

    1. Simplicity: The API is kept intentionally small. The library prioritizes a minimal set of flags and configuration options to ensure tests are easy to write, understand, and maintain. This also minimizes the frequency of breaking changes during upgrades.
    2. Reliability: To ensure consistent test results, the library attempts to control as many container aspects as possible. A key consequence of this is that for many built-in images, the container tag is not configurable. This prevents users from using tags that haven't been verified for compatibility with the library.
    3. Ease of Use: The library is designed to minimize boilerplate. Users are encouraged to implement their own Image definitions if they need custom container configurations, which is a supported and lightweight process.
  4. Choosing between copying and mounting

    main

    Decide how to handle files based on your testing requirements:

    ApproachUse Case
    Copy before startup (with_copy_to)For deterministic, static inputs required before the container runs.
    Copy from containers (copy_file_from)To capture build artifacts, logs, or test fixtures produced during the container's execution.
    Use mounts (Mount)When containers need to read/write large amounts of data efficiently without the overhead of re-tarring files.

    Mixing these approaches allows you to keep tests hermetic while still being able to inspect outputs locally.

  5. Use wait strategies to ensure container readiness

    main

    Testcontainers for Rust uses wait strategy to ensure a container has reached a specific state (e.g., a service is listening on a port or a specific log message appears) before your tests proceed.

    Strategies are defined using the WaitFor enum. You can apply a strategy to a GenericImage using the with_wait_for method.

    // Example of applying a wait strategy to a GenericImage
    let image = GenericImage::from_image("postgres:latest")
        .with_wait_for(WaitFor::Http { 
            // ... configuration for HttpWaitStrategy
        });
  6. Define lifecycle command execution in custom `Image` implementations

    main

    If you are implementing your own Image trait, you can define commands that run automatically during the container lifecycle using two methods:

    1. exec_before_ready: Executes commands after the container has started, but before the Image::ready_conditions are awaited.
    2. exec_after_start: Executes commands only after the container is started and ready.
  7. How Docker Compose support works

    main

    Testcontainers for Rust allows you to run multi-container applications defined in Docker Compose files. This is useful for testing interconnected services.

    Note: Docker Compose support is currently only available for async runtimes.

    Key lifecycle concepts:

    • Startup: Use DockerCompose::up().await? to start the stack. This method uses Docker Compose's --wait flag, meaning it will wait for services to pass their healthchecks before returning.
    • Cleanup: By default, the stack is automatically cleaned up when the DockerCompose instance is dropped. You can also call compose.down().await? for explicit teardown.
    • Isolation: Each test gets a unique project name (UUID) automatically to prevent conflicts during parallel execution. You can override this with .with_project_name("name").
  8. Spin up a container using GenericImage

    main

    Use the GenericImage struct to define and start a container. The lifecycle is managed via RAII: when the container object goes out of scope, it is automatically removed by the Drop trait.

    To prevent automatic removal (e.g., for debugging), set the TESTCONTAINERS_COMMAND environment variable to keep.

    Key methods:

    • GenericImage::new(image, tag): Initializes the image and version.
    • .with_exposed_port(port): Maps a container port to a random available port on the host (similar to docker run -p <port>). This enables parallel test execution as each container gets a unique host port.
    • .with_wait_for(condition): Defines readiness criteria using WaitFor (e.g., checking stdout for a specific message) to ensure the container is ready before the test proceeds.
    • .start(): An implementation of the AsyncRunner (or SyncRunner with the blocking feature) trait that launches the container.
    use testcontainers::{
        core::{IntoContainerPort, WaitFor},
        runners::AsyncRunner,
        GenericImage,
    };
    
    #[tokio::test]
    async fn test_redis() {
        let _container = GenericImage::new("redis", "7.2.4")
            .with_exposed_port(6379.tcp())
            .with_wait_for(WaitFor::message_on_stdout("Ready to accept connections"))
            .start()
            .await
            .unwrap();
    }
  9. Use the Containerised Client for Docker Compose

    main

    The Containerised Client runs docker compose inside a container. This is ideal for CI/CD environments where a local Docker CLI might not be available, ensuring a consistent Compose version.

    // Basic usage
    let mut compose = DockerCompose::with_containerised_client(&["docker-compose.yml"]).await;
    compose.up().await?;
    
    // Using options to set a project directory (required if using relative paths for bind mounts)
    use testcontainers::compose::{ContainerisedComposeOptions, DockerCompose};
    
    let options = ContainerisedComposeOptions::new(&["/home/me/app/docker-compose.yml"])
        .with_project_directory("/home/me/app");
    
    let mut compose = DockerCompose::with_containerised_client(options).await?;
    compose.up().await?;
  10. Use the Local Client for Docker Compose

    main

    The Local Client is the default mode. It uses the docker compose CLI installed on your local machine.

    Requirements:

    • Docker CLI with the Compose plugin must be installed locally.
    • Compose files must exist on the local filesystem.
    use testcontainers::compose::DockerCompose;
    
    let mut compose = DockerCompose::with_local_client(&["docker-compose.yml"]);
    compose.up().await?;
  11. Execute commands in running containers using `exec`

    main

    The most common way to run commands within an already running container is to use the .exec() method available on ContainerAsync or Container. This method requires an ExecCommand struct and returns an ExecResult (for async) or SyncExecResult (for the blocking feature).

    let result = container.exec(command).await?;
    let exit_code = result.exit_code().await?;
    let stdout = result.stdout_to_vec().await?;
    let stderr = result.stderr_to_vec().await?;