Harnessing the Power of Testcontainers for Robust Testing in Go
Tags: go, docker, testing, programming
Published: 2023-12-13 21:32:57

> Testcontainers for Go is a Go package that makes it simple to create and clean up container-based dependencies for automated integration/smoke tests. The clean, easy-to-use API enables developers to programmatically define containers that should be run as part of a test and clean up those resources when the test is done.

https://golang.testcontainers.org/

## Why Testcontainers?

Testing is a critical phase in software development. While there are many different types of testing, the two that most developers are familiar with are **Unit Testing** and **Integration Testing**.

Traditionally, Unit Tests test code at the function level, while Integration Tests test code as it interacts with various external systems and dependencies, ensuring that all of the components of an application work seamlessly together.

Purists would argue that any test that requires an external dependency is an Integration Test. However, I've come to believe that at the end of the day, it doesn't matter what you call the type of test, just that your application is well tested, covering the various real-world scenarios that can occur.

This is where Testcontainers really shines. You can set up almost any scenario that you can think of for your tests that interact with external dependencies, and control those scenarios with Go code!

## Set Up

### Installation

To get started with Testcontainers-Go, you'll need to have Docker installed on your system. Then, installing the library is straightforward. Simply run:

```go
go get github.com/testcontainers/testcontainers-go
```

This command fetches the library and adds it to your project.

### Writing Your First Test

Let's imagine your application requires PostgreSQL. Instead of setting up a PostgreSQL instance manually in CI or on your local machine, you can use Testcontainers-Go to create a Docker container for PostgreSQL only for the duration of your test.

Here’s a simple example:

```go
package main

import (
    "context"
    "database/sql"
    "log"
    "testing"

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

func TestPostgreSQLContainer(t *testing.T) {
    ctx := context.Background()

    // Define the container request
    req := testcontainers.ContainerRequest{
        Image:        "postgres",
        ExposedPorts: []string{"5432/tcp"},
        Env:          map[string]string{"POSTGRES_PASSWORD": "password"},
        WaitingFor:   wait.ForLog("database system is ready to accept connections"),
    }

    // Start the container
    postgresContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: req,
        Started:          true,
    })
    if err != nil {
        log.Fatalf("Failed to start container: %s", err)
    }
    defer postgresContainer.Terminate(ctx)

    // Connect to the database
    port, _ := postgresContainer.MappedPort(ctx, "5432")
    db, _ := sql.Open("postgres", fmt.Sprintf("host=localhost port=%s user=postgres password=password dbname=postgres sslmode=disable", port.Port()))
    defer db.Close()

    // Your test logic here
}
```

In this example, we create a PostgreSQL container and wait for it to be ready. We then connect to the database and perform our test operations.

## How to Leverage Testcontainers

Now that you can spin up a PostgreSQL instance for each test, you can do things like simulate what happens if PostgreSQL becomes unreachable by programmatically pausing or stopping the container while your application is in the middle of a database operation. This action can be easily done through Testcontainers-Go's control over the Docker container lifecycle.

By pausing or stopping the container, you simulate a network failure or a database crash. Your test code would then observe and assert the behavior of your application under these conditions. Key aspects to look for include how the application handles the loss of connection (e.g., does it retry, how does it log the error, does it alert the user?), and how it recovers once the database connection is re-established. This process would be repeated under various conditions and configurations to thoroughly test the application's resilience and error-handling mechanisms.

## Conclusion

[Testcontainers](https://golang.testcontainers.org/) offers a robust solution for testing more complicated scenarios in Go applications. Its ability to create and manage Docker containers on the fly streamlines the testing process, making it more efficient and reliable. Whether you're dealing with simple database interactions or complex microservice architectures, Testcontainers-Go is a tool worth integrating into your testing suite.

**Interested in seeing Testcontainers in a real-world project?** 

Check out [Flipt](https://github.com/flipt-io/flipt), an enterprise-ready, GRPC-powered, GitOps-enabled, feature management solution written in Go where we make heavy use of Testcontainers for our tests.