Testcontainers
Testcontainers for Rust plays well with the native cargo test
.
The ideal use case is for integration or end to end tests. It helps you to spin up and manage the dependencies life cycle via Docker.
1. System requirements¶
Please read the system requirements page before you start.
2. Install Testcontainers for Rust¶
- If your tests are async:
cargo add testcontainers
- If you don't use async and want to use the blocking API:
cargo add testcontainers --features blocking
3. Spin up Redis¶
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();
}
Here we use the GenericImage
struct to create a Redis container.
GenericImage::new
accepts the image name and tag.with_exposed_port
adds a port to be exposed from the container (can be called multiple times).with_wait_for
allows to pass conditions (WaitFor
) of container rediness. It is important to get this set because it helps to know when the container is ready to receive any traffic. In this case, we check for the logs we know come from Redis, telling us that it is ready to accept requests.start
is a function of theAsyncRunner
trait that starts the container. The same logic is applicable forSyncRunner
if you are usingblocking
feature.
When you use with_exposed_port
you have to imagine yourself using docker run -p
<port>
. When you do so, dockerd
maps the selected <port>
from inside the
container to a random one available on your host.
In the previous example, we expose 6379
for tcp
traffic to the outside. This
allows Redis to be reachable from your code that runs outside the container, but
it also makes parallelization possible. When you run multiple cargo tests in parallel,
each test starts a Redis container, and each of them is exposed on a different random port.
All the containers must be removed at some point, otherwise they will run until
the host is overloaded. In order to provide a clean environment, we rely on RAII
semantic
of containers (Drop
trait). Thus, when the container goes out of scope, it is removed by default.
However, you can change this behavior by setting the TESTCONTAINERS_COMMAND
environment
variable to keep
.
4. Make your code to talk with the container¶
We will use redis as a client in this example. This code gets the endpoint from the container we just started, and it configures the client.
This is just an example, you can choose any client you want (e.g
fred
)
use redis::Client;
use testcontainers::{core::{IntoContainerPort, WaitFor}, runners::AsyncRunner, GenericImage};
#[tokio::test]
async fn test_redis() -> Result<(), Box<dyn std::error::Error + 'static>> {
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;
let host = container.get_host()?;
let host_port = container.get_host_port_ipv4(REDIS_PORT)?;
let url = format!("redis://{host}:{host_port}");
let client = redis::Client::open(url.as_ref())?;
// do something with the client
}
get_host
returns the host that this container may be reached on (may not be the local machine). In most of the cases it will belocalhost
.get_host_port_ipv4
returns the mapped host port for an internal port of this docker container. In this case it returns the port that was exposed by the container.
5. Run the test¶
You can run the test via cargo test