Four Key Reasons to Learn Markdown
Back-End Leveling UpWriting documentation is fun—really, really fun. I know some engineers may disagree with me, but as a technical writer, creating quality documentation that will...
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?
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.
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:
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.
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.
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.
Writing documentation is fun—really, really fun. I know some engineers may disagree with me, but as a technical writer, creating quality documentation that will...
Humanity has come a long way in its technological journey. We have reached the cusp of an age in which the concepts we have...
Go 1.18 has finally landed, and with it comes its own flavor of generics. In a previous post, we went over the accepted proposal and dove...