Search

Using Dockertest With Golang

John Iwasyk

4 min read

Jul 20, 2021

Using Dockertest With Golang

Why should Dockertest be part of your next Golang web project?

When designing a web application, a strategy that has often been used when testing is to mock third-party dependencies. There are many benefits to doing this such as producing hard-to-provoke responses on-demand; however getting actual responses seems better than simulating them. Why might a developer want to use Dockertest as an alternative for producing realistic responses?

Testing Strategies

One of the main strategies for testing in Go is to instantiate a test server and mock responses for specific paths related to the requests under test. While this strategy works fine for simulating errors and edge-cases, it would be nice to not worry about creating fake responses for each request. In addition, developers could make mistakes producing these responses which could be problematic for integration tests. Developers write these tests as a final check to verify that applications produce correct results given many scenarios.

Dockertest is a library meant to help accomplish this goal. By creating actual instances of these third party services through Docker containers, realistic responses can be obtained. The example below shows the setup of the container using the Dockertest library and how it is used for testing.

An Example of Using Dockertest

The below example demonstrates how Dockertest is utilized for testing a simple CRUD application. This application, a phonebook, manages phone numbers using Postgres as the external database. The code in this article is part of a larger runnable demo available in BNR-Blog-Dockertest. It relies on:

Test Setup

The test file will verify the CRUD functionality of this phonebook and ensure the storage code actually manages to store data in an actual Postgres database, that is, properly integrates with Postgres. This is done by running Postgres in a Docker container alongside the test process. Before any test runs, a Docker connection must be established and the Postgres container launched with the configuration expected by the test code:

var testPort string

const testUser = "postgres"
const testPassword = "password"
const testHost = "localhost"
const testDbName = "phone_numbers"

// getAdapter retrieves the Postgres adapter with test credentials
func getAdapter() (*PgAdapter, error) {
    return NewAdapter(testHost, testPort, testUser, testDbName, WithPassword(testPassword))
}

// setup instantiates a Postgres docker container and attempts to connect to it via a new adapter
func setup() *dockertest.Resource {
    pool, err := dockertest.NewPool("")
    if err != nil {
        log.Fatalf("could not connect to docker: %s", err)
    }

    // Pulls an image, creates a container based on it and runs it
    resource, err := pool.Run("postgres", "13", []string{fmt.Sprintf("POSTGRES_PASSWORD=%s", testPassword), fmt.Sprintf("POSTGRES_DB=%s", testDbName)})
    if err != nil {
        log.Fatalf("could not start resource: %s", err)
    }
    testPort = resource.GetPort("5432/tcp") // Set port used to communicate with Postgres

    var adapter *PgAdapter
    // Exponential backoff-retry, because the application in the container might not be ready to accept connections yet
    if err := pool.Retry(func() error {
        adapter, err = getAdapter()
        return err
    }); err != nil {
        log.Fatalf("could not connect to docker: %s", err)
    }

    initTestAdapter(adapter)

    return resource
}

func TestMain(m *testing.M) {
    setup()
    code := m.Run()
    os.Exit(code)
}

TestMain() ensures the setup() function runs before any test runs. Within the setup() function, a pool resource is created to represent a connection to the docker API used for pulling Docker images. The internal pool client pulls the latest Postgres image with specified environment options and then starts the container based on this image. POSTGRES_PASSWORD is set to testPassword which specifies the password for connecting to Postgres. POSTGRES_DB specifies the database name to be created on startup and is set to a constant: phone_numbers. The container port value is the port published for communication with Postgres based on the service port value. Afterward, the postgres instance instantiates and attempts to connect to the new docker container. If there is an error doing this, the test suite exits with an error.

An example of a test that utilizes the Postgres instance is below.

Test

func TestCreatePhoneNumber(t *testing.T) {
    testNumber := "1234566656"
    adapter, err := getAdapter()
    if err != nil {
        t.Fatalf("error creating new test adapter: %v", err)
    }

    cases := []struct {
        error       bool
        description string
    }{
        {
            description: "Should succeed with valid creation of a phone number",
        },
        {
            description: "Should fail if database connection closed",
            error:       true,
        },
    }
    for _, c := range cases {
        t.Run(c.description, func(t *testing.T) {
            if c.error {
                adapter.conn.Close()
            }
            id, err := adapter.CreatePhoneNumber(testNumber)
            if !c.error && err != nil {
                t.Errorf("expecting no error but received: %v", err)
            } else if !c.error { // Remove test number from db so not captured by following tests
                err = adapter.RemovePhoneNumber(id)
                if err != nil {
                    t.Fatalf("error removing test number from database")
                }
            }
        })
    }
}

The table-driven test case above is defined to verify the create method of the postgres storage adapter. The first case assumes that a test phone number successfully inserts into the docker Postgres instance. The second case forces the database connection to close and then assumes that the create method fails on the Postgres instance.

Conclusion

In summary, mocking is a fine way to test third-party dependencies but using a library such as Dockertest can allow for a more realistic and robust integration testing environment. With the capability to launch any Docker container, an entire portion of a web application can be tested with real results in a controlled test environment. Such a library can be useful within a unit test or integration test environment. Dockertest can also be set up in CI environments, as with GitHub Actions’ service containers. For more examples, see the Dockertest repository.

Mark Dalrymple

Reviewer Big Nerd Ranch

MarkD is a long-time Unix and Mac developer, having worked at AOL, Google, and several start-ups over the years.  He’s the author of Advanced Mac OS X Programming: The Big Nerd Ranch Guide, over 100 blog posts for Big Nerd Ranch, and an occasional speaker at conferences. Believing in the power of community, he’s a co-founder of CocoaHeads, an international Mac and iPhone meetup, and runs the Pittsburgh PA chapter. In his spare time, he plays orchestral and swing band music.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News