Testcontainers are a wrapper around the Docker daemon designed for tests. Anything you can run in Docker, you can spin up with Testcontainers and integrate into your tests:
- NoSQL databases or other data stores (e.g. Redis, ElasticSearch, MongoDB)
- Web servers/proxies (e.g. NGINX, Apache)
- Log services (e.g. Logstash, Kibana)
- Other services developed by your team/organization which are already dockerized
- Since :material-tag: v0.37.0
testcontainers.Run defines the container that should be run, similar to the docker run command.
func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*DockerContainer, error)context.Context, the Go context.string, the Docker image to use.testcontainers.ContainerCustomizer, a variadic argument for passing options.
The following test creates an NGINX container on both the bridge (docker default
network) and the foo network and validates that it returns 200 for the status code.
It also demonstrates how to use CleanupContainer, that ensures that nginx container
is removed when the test ends even if the underlying container errored,
as well as the CleanupNetwork which does the same for networks.
The alternatives for these outside of tests as a defer are TerminateContainer
and Network.Remove which can be seen in the examples.
Creating a container inside_block:ExampleRun
{% include "../features/common_functional_options.md" %}
!!!warning
GenericContainer is the old way to create a container, and we recommend using Run instead,
as it could be deprecated in the future.
testcontainers.GenericContainer defines the container that should be run, similar to the docker run command.
The following test creates an NGINX container on both the bridge (docker default
network) and the foo network and validates that it returns 200 for the status code.
It also demonstrates how to use CleanupContainer ensures that nginx container
is removed when the test ends even if the underlying GenericContainer errored
as well as the CleanupNetwork which does the same for networks.
The alternatives for these outside of tests as a defer are TerminateContainer
and Network.Remove which can be seen in the examples.
package main
import (
"context"
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
type nginxContainer struct {
testcontainers.Container
URI string
}
func setupNginx(ctx context.Context, networkName string) (*nginxContainer, error) {
req := testcontainers.ContainerRequest{
Image: "nginx",
ExposedPorts: []string{"80/tcp"},
Networks: []string{"bridge", networkName},
WaitingFor: wait.ForHTTP("/"),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
var nginxC *nginxContainer
if container != nil {
nginxC = &nginxContainer{Container: container}
}
if err != nil {
return nginxC, err
}
ip, err := container.Host(ctx)
if err != nil {
return nginxC, err
}
mappedPort, err := container.MappedPort(ctx, "80")
if err != nil {
return nginxC, err
}
nginxC.URI = fmt.Sprintf("http://%s:%s", ip, mappedPort.Port())
return nginxC, nil
}
func TestIntegrationNginxLatestReturn(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
nw, err := network.New(ctx)
require.NoError(t, err)
testcontainers.CleanupNetwork(t, nw)
nginxC, err := setupNginx(ctx, nw.Name)
testcontainers.CleanupContainer(t, nginxC)
require.NoError(t, err)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, nginxC.URI, http.NoBody)
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
require.NoError(t, resp.Body.Close())
}Testcontainers for Go allows you to define your own lifecycle hooks for better control over your containers. You just need to define functions that return an error and receive the Go context as the first argument.
You'll be able to pass multiple lifecycle hooks using the WithLifecycleHooks and WithAdditionalLifecycleHooks options, passing an array of testcontainers.ContainerLifecycleHooks. The first one replaces the existing lifecycle hooks with the new ones, while the second one appends the new lifecycle hooks to the existing ones.
The testcontainers.ContainerLifecycleHooks struct defines the following lifecycle hooks, each of them backed by an array of functions representing the hooks:
PreBuilds- hooks that are executed before the image is built. This hook is only available when creating a container from a DockerfilePostBuilds- hooks that are executed after the image is built. This hook is only available when creating a container from a DockerfilePreCreates- hooks that are executed before the container is createdPostCreates- hooks that are executed after the container is createdPreStarts- hooks that are executed before the container is startedPostStarts- hooks that are executed after the container is startedPostReadies- hooks that are executed after the container is readyPreStops- hooks that are executed before the container is stoppedPostStops- hooks that are executed after the container is stoppedPreTerminates- hooks that are executed before the container is terminatedPostTerminates- hooks that are executed after the container is terminated
Testcontainers for Go defines some default lifecycle hooks that are always executed in a specific order with respect to the user-defined hooks. The order of execution is the following:
- default
prehooks. - user-defined
prehooks. - user-defined
posthooks. - default
posthooks.
Inside each group, the hooks will be executed in the order they were defined.
!!!info The default hooks are for logging (applied to all hooks), customising the Docker config (applied to the pre-create hook), copying files in to the container (applied to the post-create hook), adding log consumers (applied to the post-start and pre-terminate hooks), and running the wait strategies as a readiness check (applied to the post-start hook).
It's important to note that the Readiness of a container is defined by the wait strategies configured for the container. This hook is executed right after the PostStarts hook. If you want additional readiness checks, add a PostReadies hook, which runs after the default ones. The PostStarts hooks do not imply readiness; don't rely on them for that.
!!!warning
Up to v0.37.0, the readiness hook included checks for all the exposed ports to be ready. This is not the case anymore, and the readiness hook only uses the wait strategies defined for the container to determine if the container is ready.
In the following example, we are going to create a container using all the lifecycle hooks, all of them printing a message when any of the lifecycle hooks is called:
Extending container with lifecycle hooks inside_block:optsWithLifecycleHooks
Testcontainers for Go comes with a default logging hook that will print a log message for each container lifecycle event, using the default logger. You can add your own logger by passing the testcontainers.DefaultLoggingHook option to the Run options, passing a reference to your preferred logger:
Use a custom logger for container hooks inside_block:optsWithDefaultLoggingHook Custom Logger implementation inside_block:customLoggerImplementation
The aforementioned Run function represents a straightforward way to configure containers, but you may need more advanced settings regarding the Docker config, host config, and endpoint settings types. For those advanced settings, Testcontainers for Go offers a way to fully customize the container and those internal Docker types. These customisations, called modifiers, are applied just before the internal call to the Docker client to create the container.
Using modifiers inside_block:reqWithModifiers
!!!warning
The only special case where the modifiers are not applied last, is when there are no exposed ports and the container does not use a network mode from a container (e.g. req.NetworkMode = container.NetworkMode("container:$CONTAINER_ID")). In that case, Testcontainers for Go will extract the ports from the underlying Docker image and export them.
Using the WithReuseByName option you can reuse an existing container. Reuse works only when you provide an
existing container name to this option. If the name is not found among existing containers,
the function will create a new container. If the name is empty, an error is returned.
The following test creates an NGINX container, adds a file into it and then reuses the container again for checking the file:
package main
import (
"context"
"fmt"
"log"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
const (
reusableContainerName = "my_test_reusable_container"
)
func main() {
ctx := context.Background()
n1, err := testcontainers.Run(ctx, "nginx:1.17.6",
testcontainers.WithExposedPorts("80/tcp"),
testcontainers.WithWaitStrategy(wait.ForListeningPort("80/tcp")),
testcontainers.WithReuseByName(reusableContainerName),
)
defer func() {
if err := testcontainers.TerminateContainer(n1); err != nil {
log.Printf("failed to terminate container: %s", err)
}
}()
if err != nil {
log.Print(err)
return
}
copiedFileName := "hello_copy.sh"
err = n1.CopyFileToContainer(ctx, "./testdata/hello.sh", "/"+copiedFileName, 700)
if err != nil {
log.Print(err)
return
}
n2, err := testcontainers.Run(ctx, "nginx:1.17.6",
testcontainers.WithExposedPorts("80/tcp"),
testcontainers.WithWaitStrategy(wait.ForListeningPort("80/tcp")),
testcontainers.WithReuseByName(reusableContainerName),
)
defer func() {
if err := testcontainers.TerminateContainer(n2); err != nil {
log.Printf("failed to terminate container: %s", err)
}
}()
if err != nil {
log.Print(err)
return
}
c, _, err := n2.Exec(ctx, []string{"bash", copiedFileName})
if err != nil {
log.Print(err)
return
}
fmt.Println(c)
}testcontainers.ParallelContainers - defines the containers that should be run in parallel mode.
The following test creates two NGINX containers in parallel:
package main
import (
"context"
"fmt"
"log"
"github.com/testcontainers/testcontainers-go"
)
func main() {
ctx := context.Background()
requests := testcontainers.ParallelContainerRequest{
{
ContainerRequest: testcontainers.ContainerRequest{
Image: "nginx",
ExposedPorts: []string{
"10080/tcp",
},
},
Started: true,
},
{
ContainerRequest: testcontainers.ContainerRequest{
Image: "nginx",
ExposedPorts: []string{
"10081/tcp",
},
},
Started: true,
},
}
res, err := testcontainers.ParallelContainers(ctx, requests, testcontainers.ParallelContainersOptions{})
for _, c := range res {
c := c
defer func() {
if err := testcontainers.TerminateContainer(c); err != nil {
log.Printf("failed to terminate container: %s", c)
}
}()
}
if err != nil {
e, ok := err.(testcontainers.ParallelContainersError)
if !ok {
log.Printf("unknown error: %v", err)
return
}
for _, pe := range e.Errors {
fmt.Println(pe.Request, pe.Error)
}
return
}
}