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/testcontainers/testcontainers-go"
	"github.com/testcontainers/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.
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",
		},
	}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
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!"},
}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
}