Categorygithub.com/romnnn/testcontainers-go
modulepackage
0.5.2
Repository: https://github.com/romnnn/testcontainers-go.git
Documentation: pkg.go.dev

# README

Build Status

When I was working on a Zipkin PR I discovered a nice Java library called testcontainers.

It provides an easy and clean API over the go docker sdk to run, terminate and connect to containers in your tests.

I found myself comfortable programmatically writing the containers I need to run an integration/smoke tests. So I started porting this library in Go.

This is the API I have defined:

package main

import (
	"context"
	"fmt"
	"net/http"
	"testing"

	"github.com/romnnn/testcontainers-go"
	"github.com/romnnn/testcontainers-go/wait"
)

func TestNginxLatestReturn(t *testing.T) {
	ctx := context.Background()
	req := testcontainers.ContainerRequest{
		Image:        "nginx",
		ExposedPorts: []string{"80/tcp"},
		WaitingFor:   wait.ForHTTP("/"),
	}
	nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
	})
	if err != nil {
		t.Error(err)
	}
	defer nginxC.Terminate(ctx)
	ip, err := nginxC.Host(ctx)
	if err != nil {
		t.Error(err)
	}
	port, err := nginxC.MappedPort(ctx, "80")
	if err != nil {
		t.Error(err)
	}
	resp, err := http.Get(fmt.Sprintf("http://%s:%s", ip, port.Port()))
	if resp.StatusCode != http.StatusOK {
		t.Errorf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode)
	}
}

This is a simple example, you can create one container in my case using the nginx image. You can get its IP ip, err := nginxC.GetContainerIpAddress(ctx) and you can use it to make a GET: resp, err := http.Get(fmt.Sprintf("http://%s", ip))

To clean your environment you can defer the container termination defer nginxC.Terminate(ctx, t). t is *testing.T and it is used to notify is the defer failed marking the test as failed.

Build from Dockerfile

Testcontainers-go gives you the ability to build an image and run a container from a Dockerfile.

You can do so by specifying a Context (the filepath to the build context on your local filesystem) and optionally a Dockerfile (defaults to "Dockerfile") like so:

req := ContainerRequest{
		FromDockerfile: testcontainers.FromDockerfile{
			Context: "/path/to/build/context",
			Dockerfile: "CustomDockerfile",
		},
	}

Dynamic Build Context

If you would like to send a build context that you created in code (maybe you have a dynamic Dockerfile), you can send the build context as an io.Reader since the Docker Daemon accepts is as a tar file, you can use the tar package to create your context.

To do this you would use the ContextArchive attribute in the FromDockerfile struct.

var buf bytes.Buffer
tarWriter := tar.NewWriter(&buf)
// ... add some files
if err := tarWriter.Close(); err != nil {
	// do something with err
}
reader := bytes.NewReader(buf.Bytes())
fromDockerfile := testcontainers.FromDockerfile{
	ContextArchive: reader,
}

Please Note if you specify a ContextArchive this will cause testcontainers to ignore the path passed in to Context

Sending a CMD to a Container

If you would like to send a CMD (command) to a container, you can pass it in to the container request via the Cmd field...

req := ContainerRequest{
	Image: "alpine",
	WaitingFor: wait.ForAll(
		wait.ForLog("command override!"),
	),
	Cmd: []string{"echo", "command override!"},
}

Following Container Logs

If you wish to follow container logs, you can set up LogConsumers. The log following functionality follows a producer-consumer model. You will need to explicitly start and stop the producer. As logs are written to either stdout, or stderr (stdin is not supported) they will be forwarded (produced) to any associated LogConsumers. You can associate LogConsumers with the .FollowOutput function.

Please note if you start the producer you should always stop it explicitly.

for example, this consumer will just add logs to a slice

type TestLogConsumer struct {
	Msgs []string
}

func (g *TestLogConsumer) Accept(l Log) {
	g.Msgs = append(g.Msgs, string(l.Content))
}

this can be used like so:

g := TestLogConsumer{
	Msgs: []string{},
}

err := c.StartLogProducer(ctx)
if err != nil {
	// do something with err
}

c.FollowOutput(&g)

// some stuff happens...

err = c.StopLogProducer()
if err != nil {
	// do something with err
}

Using Docker Compose

Similar to generic containers support, it's also possible to run a bespoke set of services specified in a docker-compose.yml file.

This is intended to be useful on projects where Docker Compose is already used in dev or other environments to define services that an application may be dependent upon.

You can override Testcontainers' default behaviour and make it use a docker-compose binary installed on the local machine. This will generally yield an experience that is closer to running docker-compose locally, with the caveat that Docker Compose needs to be present on dev and CI machines.

Examples

composeFilePaths := "testresources/docker-compose.yml"
identifier := strings.ToLower(uuid.New().String())

compose := tc.NewLocalDockerCompose(composeFilePaths, identifier)
execError := compose.
	WithCommand([]string{"up", "-d"}).
	WithEnv(map[string]string {
		"key1": "value1",
		"key2": "value2",
	}).
	Invoke()
err := execError.Error
if err != nil {
	return fmt.Errorf("Could not run compose file: %v - %v", composeFilePaths, err)
}
return nil

Note that the environment variables in the env map will be applied, if possible, to the existing variables declared in the docker compose file.

In the following example, we demonstrate how to stop a Docker compose using the convenient Down method.

composeFilePaths := "testresources/docker-compose.yml"

compose := tc.NewLocalDockerCompose(composeFilePaths, identifierFromExistingRunningCompose)
execError := compose.Down()
err := execError.Error
if err != nil {
	return fmt.Errorf("Could not run compose file: %v - %v", composeFilePaths, err)
}
return nil

Troubleshooting Travis

If you want to reproduce a Travis build locally, please follow this instructions to spin up a Travis build agent locally:

export BUILDID="build-testcontainers"
export INSTANCE="travisci/ci-sardonyx:packer-1564753982-0c06deb6"
docker run --name $BUILDID -w /root/go/src/github.com/romnnn/testcontainers-go -v /Users/mdelapenya/sourcecode/src/github.com/mdelapenya/testcontainers-go:/root/go/src/github.com/romnnn/testcontainers-go -v /var/run/docker.sock:/var/run/docker.sock -dit $INSTANCE /sbin/init

Once the container has been created, enter it (docker exec -ti $BUILDID bash) and reproduce Travis steps:

eval "$(gimme 1.11.4)"
export GO111MODULE=on
export GOPATH="/root/go"
export PATH="$GOPATH/bin:$PATH"
go get gotest.tools/gotestsum
go mod tidy
go fmt ./...
go vet ./...
gotestsum --format short-verbose ./...

# Packages

No description provided by the author
No description provided by the author

# Functions

GenericContainer creates a generic container with parameters.
GenericNetwork creates a generic network with parameters.
NewDockerProvider creates a Docker provider with the EnvClient.
NewLocalDockerCompose returns an instance of the local Docker Compose, using an array of Docker Compose file paths and an identifier for the Compose execution.
NewReaper creates a Reaper with a sessionID to identify containers and a provider to use.

# Constants

Docker is default = 0.
No description provided by the author
StderrLog is the log type for STDERR.
StdoutLog is the log type for STDOUT.
No description provided by the author
No description provided by the author
No description provided by the author

# Structs

ContainerRequest represents the parameters used to get a running container.
ContainerResourcers represents the host resources a running container is allowed to consume.
DockerContainer represents a container started using Docker.
DockerNetwork represents a network started using Docker.
DockerProvider implements the ContainerProvider interface.
ExecError is super struct that holds any information about an execution error, so the client code can handle the result.
FromDockerfile represents the parameters needed to build an image from a Dockerfile rather than using a pre-built one.
GenericContainerRequest represents parameters to a generic container.
GenericNetworkRequest represents parameters to a generic network.
LocalDockerCompose represents a Docker Compose execution using local binary docker-compose or docker-compose.exe, depending on the underlying platform.
Log represents a message that was created by a process, LogType is either "STDOUT" or "STDERR", Content is the byte contents of the message itself.
NetworkRequest represents the parameters used to get a network.
Reaper is used to start a sidecar container that cleans up resources.

# Interfaces

Container allows getting info about and controlling a single container instance.
ContainerProvider allows the creation of containers on an arbitrary system.
DeprecatedContainer shows methods that were supported before, but are now deprecated Deprecated: Use Container.
DockerCompose defines the contract for running Docker Compose.
GenericProvider represents an abstraction for container and network providers.
ImageBuildInfo defines what is needed to build an image.
LogConsumer represents any object that can handle a Log, it is up to the LogConsumer instance what to do with the log.
Network allows getting info about a single network instance.
NetworkProvider allows the creation of networks on an arbitrary system.
ReaperProvider represents a provider for the reaper to run itself with The ContainerProvider interface should usually satisfy this as well, so it is pluggable.

# Type aliases

ProviderType is an enum for the possible providers.