Getting started with Testcontainers for Go
Introduction
Testcontainers is a powerful open source library designed for integration testing in your Go applications. This library provides an easy way to create and manage ephemeral Docker containers, allowing you to simulate various dependencies like databases, message brokers, or other services required for your application's functionality during the testing phase. By using Testcontainers, you can test your application in an environment that closely resembles your production setup, reducing the need for mocks or expensive testing environments.
In this post, I will walk you through the process of getting started with Testcontainers, focusing on how to incorporate it into your Go project. We will begin by creating a new Go project and adding the Testcontainers library as a dependency. Then, I will demonstrate how to create and interact with a Docker container running a simple HTTP server as part of your tests.
You can find the source code for this tutorial on GitHub.
Creating the project
Let's start by setting up a new project to explore Testcontainers in action.
Initializing a new Go module
We'll use Go Modules to manage our dependencies. To begin, create a new directory and initialize a new Go module:
mkdir testcontainers-go-getting-started
cd testcontainers-go-getting-started
go mod init github.com/marcnuri-demo/testcontainers-go-getting-started
If the process goes smoothly, you should have a new go.mod
file with the following content:
module github.com/marcnuri-demo/testcontainers-go-getting-started
go 1.21.2
Creating the main package
Next, we'll create a new file main_test.go
,
which will serve as the foundation for our test implementation:
package main
import "testing"
func TestExample(t *testing.T) {
}
Adding the Testcontainers dependency
Now that we have the test structure in place, we can add the Testcontainers library as a dependency.
To do so, use the go get
command:
go get github.com/testcontainers/testcontainers-go
This command will include the Testcontainers dependency in your go.mod
file and bootstrap a go.sum
file containing the dependency checksums.
Implementing the test
With the project structure and Testcontainers dependency in place, we can start working on our test.
We'll start by seeing how to spin up a new container running a simple HTTP server.
Spinning up a new container
Let's modify the TestExample
function to include a Testconainers request for creating a new container:
func TestExample(t *testing.T) {
containerCtx := context.Background()
chuckNorris, _ := testcontainers.GenericContainer(containerCtx, testcontainers.GenericContainerRequest{
Started: true,
ContainerRequest: testcontainers.ContainerRequest{
Image: "marcnuri/chuck-norris:latest",
ExposedPorts: []string{"8080/tcp"},
WaitingFor: wait.ForLog("Listening on: http://0.0.0.0:8080"),
},
})
defer func() {
err := chuckNorris.Terminate(containerCtx)
if err != nil {
panic(err)
}
}()
}
Here's a breakdown of what's happening in the code above:
- We initiate a Go context called
containerCtx
that will be used by Testcontainers to manage the information about the container lifecycle. - Using the
GenericContainer
function, we create a new container. This function requires a Go context as its first argument. The second parameter is aGenericContainerRequest
struct with the following fields:Started
: A boolean that indicates whether the container should be started as soon as it's created. Since we want the container to start automatically, we set it totrue
.ContainerRequest
: AContainerRequest
struct that contains the information about the container we want to spin up. In this case, we want to create a new container using themarcnuri/chuck-norris:latest
image which exposes an HTTP API on port8080
.
Let's take a more detailed look at the ContainerRequest
struct fields:
Image
: The Docker image to use for the container.ExposedPorts
: An array specifying the ports to expose from the container.WaitingFor
: ALogStrategy
struct that will be used to wait for the container to be ready. In this example, we use theForLog
function to wait for the container to log the messageListening on: http://0.0.0.0:8080
which is the message that the container logs when it's ready to accept requests.
Finally, we defer a function to ensure the container is terminated after the test concludes, regardless of the test's outcome.
You can execute the tests by running the following command:
go test
If all goes well, you should see an output similar to the following:
github.com/testcontainers/testcontainers-go - Connected to docker:
Resolved Docker Host: unix:///var/run/docker.sock
Resolved Docker Socket Path: /var/run/docker.sock
Test SessionID: 459a37cc7f20d8c0f04f777e23bcea0cfe0556ba695e226e643a700ed927b4b1
Test ProcessID: 52bf9bdd-48a2-4744-8f4e-52cfef44a17d
🐳 Creating container for image marcnuri/chuck-norris:latest
✅ Container created: a7a9ef65cf1a
🐳 Starting container: a7a9ef65cf1a
✅ Container started: a7a9ef65cf1a
🚧 Waiting for container id a7a9ef65cf1a image: marcnuri/chuck-norris:latest. Waiting for: &{timeout:<nil> Log:Listening on: http://0.0.0.0:8080 IsRegexp:false Occurrence:1 PollInterval:100ms}
🐳 Terminating container: a7a9ef65cf1a
🚫 Container terminated: a7a9ef65cf1a
Making requests to the container
Now that we have a running container, let's see how to interact with it.
We'll modify the TestExample
function to make a request to the container and validate its response:
func TestExample(t *testing.T) {
// ... Testcontainers initialization code
endpoint, err := chuckNorris.Endpoint(containerCtx, "http")
if err != nil {
t.Error(err)
}
response, err := http.Get(endpoint)
if err != nil {
t.Error(err)
}
if response.StatusCode != http.StatusOK {
t.Errorf("Expected status code %d, got %d", http.StatusOK, response.StatusCode)
}
body, err := io.ReadAll(response.Body)
if err != nil {
t.Error(err)
}
bodyString := string(body)
if !strings.Contains(strings.ToLower(bodyString), "chuck") {
t.Errorf("Expected a Chuck Norris approved response, got \"%s\"", bodyString)
}
}
The code snippet above does the following:
- Using the
Endpoint
function, we retrieve the container's endpoint. This function requires a Go context as its first argument and a protocol as its second argument. In the example, we want to retrieve the HTTP endpoint, so we passhttp
as the second argument. - Using the
http.Get
function, we make a request to the container's endpoint. - We validate that the response status code is
200 OK
. - We read the response body and validate that it contains the word
Chuck
.
You should be able to execute the tests once again by running go test
.
Everything should work as expected, and you should see the test passing.
Conclusion
In this article, we've seen how to get started with Testcontainers for Go. We began by creating a new Go project from scratch and adding the Testcontainers dependency. Then, we implemented a simple test that spins up a new container and makes an HTTP request to it. You should now have a solid foundation for incorporating Testcontainers into your Go projects.
You can find the full source code for this post at GitHub.
Happy testing!