Skip to main content

Using Docker Compose with TestContainers-go

·2 mins

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.