Using Docker Compose with TestContainers-go
TestContainers-go is a Go package designed to simplify the management of container-based dependencies in automated tests: it does so by providing a DSL wrapping the Docker daemon, so that they can be created and removed in test code; in addition to that, it is also able to run a set of services declared in a Docker Compose file.
For example, the following docker-compose.yaml
file:
version: "3"
services:
postgres:
image: postgres
ports:
- 5432:5432
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
interval: 1s
timeout: 1s
retries: 3
toxiproxy:
image: shopify/toxiproxy
ports:
- 8474:8474
- 15432:15432
can be used like this:
import (
"fmt"
"strings"
tc "github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
path := "test/docker-compose.yml"
dockerComposeID := strings.ToLower(uuid.New().String())
compose := tc.NewLocalDockerCompose([]string{path}, dockerComposeID)
err := compose.
WithCommand([]string{"up", "-d"}).
WaitForService("postgres", wait.ForListeningPort("5432/tcp")).
Invoke()
if err.Error != nil {
return fmt.Errorf("Could not run compose file: %v - %v", path, err)
}
// ---
// run tests here
// ---
err := compose.Down()
if err.Error != nil {
return fmt.Errorf("Could not run compose file: %v - %v", path, err)
}
return nil
This works totally fine on a single project, but has a limitation when we run containers in parallel on the same Docker host: this can happen, for example, when running tests from 2 or more projects/pipelines at the same time on the local machine or CI/CD (if the latter uses a shared Docker host to run jobs). In those situations, we might encounter an error such as:
one or more wait strategies could not be applied to the running containers: expecting only one running container for postgres but got 2
The error is raised because, when instructed to wait for a service, TestContainers filters containers by their name or service name (see https://github.com/testcontainers/testcontainers-go/blob/main/compose.go#L179-L182) which, in this example would be postgres
.
To overcome this, we can explicitly set a unique container_name
in the docker-compose.yaml
file as follows:
version: "3"
services:
postgres:
container_name: postgres_$uid # <--
image: postgres
# ...
toxiproxy:
container_name: toxiproxy_$uid # <--
image: shopify/toxiproxy
# ...
where $uid
is a unique identifier dynamically created when setting up tests, for example by doing:
import (
"strconv"
// other imports
)
uid := strconv.FormatInt(time.Now().UnixMilli(), 10) // or UUID, or any other unique ID
err := compose.
WithCommand([]string{"up", "-d"}).
WithEnv(map[string]string {
"uid": uid,
}).
WaitForService(fmt.Sprintf("postgres_%s", uid), wait.ForListeningPort("5432/tcp")).
Invoke()
Now each service gets a unique name and we don’t risk to incur in clashes anymore.