testcontainers-rs
repository·main·Indexed 22 days ago
https://github.com/testcontainers/testcontainers-rsThe 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.
What's inside testcontainers-rs
- 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.
Understand the purpose of the testimages directory
mainThetestimagesdirectory contains the build contexts for Docker images used specifically for testing thetestcontainers-rslibrary. 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.Design principles of testcontainers-rs
mainThe
testcontainers-rslibrary is built around three core design principles to ensure a high-quality developer experience:- 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.
- 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.
- Ease of Use: The library is designed to minimize boilerplate. Users are encouraged to implement their own
Imagedefinitions if they need custom container configurations, which is a supported and lightweight process.
Choosing between copying and mounting
mainDecide how to handle files based on your testing requirements:
Approach Use 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.
Use wait strategies to ensure container readiness
mainTestcontainers for Rust uses
wait strategyto 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
WaitForenum. You can apply a strategy to aGenericImageusing thewith_wait_formethod.// Example of applying a wait strategy to a GenericImage let image = GenericImage::from_image("postgres:latest") .with_wait_for(WaitFor::Http { // ... configuration for HttpWaitStrategy });Define lifecycle command execution in custom `Image` implementations
mainIf you are implementing your own
Imagetrait, you can define commands that run automatically during the container lifecycle using two methods:exec_before_ready: Executes commands after the container has started, but before theImage::ready_conditionsare awaited.exec_after_start: Executes commands only after the container is started and ready.
Resolve the Docker host
mainIf you need to specify a non-default Docker host,
testcontainers-rsresolves the host in this specific order:- The
tc.hostproperty defined in the~/.testcontainers.propertiesfile. - The
DOCKER_HOSTenvironment variable. - The
docker.hostproperty defined in the~/.testcontainers.propertiesfile. - The default Docker socket (fallback).
- The
How Docker Compose support works
mainTestcontainers 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--waitflag, meaning it will wait for services to pass their healthchecks before returning. - Cleanup: By default, the stack is automatically cleaned up when the
DockerComposeinstance is dropped. You can also callcompose.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").
- Startup: Use
Spin up a container using GenericImage
mainUse the
GenericImagestruct 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 theDroptrait.To prevent automatic removal (e.g., for debugging), set the
TESTCONTAINERS_COMMANDenvironment variable tokeep.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 todocker run -p <port>). This enables parallel test execution as each container gets a unique host port..with_wait_for(condition): Defines readiness criteria usingWaitFor(e.g., checking stdout for a specific message) to ensure the container is ready before the test proceeds..start(): An implementation of theAsyncRunner(orSyncRunnerwith theblockingfeature) 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(); }Use the Containerised Client for Docker Compose
mainThe Containerised Client runs
docker composeinside 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?;Use the Local Client for Docker Compose
mainThe Local Client is the default mode. It uses the
docker composeCLI 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?;Execute commands in running containers using `exec`
mainThe most common way to run commands within an already running container is to use the
.exec()method available onContainerAsyncorContainer. This method requires anExecCommandstruct and returns anExecResult(for async) orSyncExecResult(for theblockingfeature).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?;