Konrad Reiche Photos About Talks

How to separate integration tests in Go

There are different ways of separating integration tests from your unit tests in Go. After discovering too many issues with some of the approaches I have settled with the following.

func TestDatabase(t *testing.T) {
  integrationTest(t)
  // ...
}

func integrationTest(t *testing.T) {
  t.Helper()
  if os.Getenv("INTEGRATION") == "" {
    t.Skip("skipping integration tests, set environment variable INTEGRATION")
 }
}

Integration tests are marked as such at the top of the test function, similar to using t.Helper(). A test helper function is used to skip the test unless the environment variable is set. All tests, including integration tests, can be run with:

INTEGRATION=1 go test ./...

What about build tags?

Too often I see build tags as suggestion to separate integration tests but they come with many drawbacks. The build constraint applies to the whole file, making it impossible to mark a subset of tests as integration tests in the same file.

//go:build integration
package store

The Go compiler or linter will skip build constrained files by default. Not only does this break refactoring like renaming, but also breaking changes that would otherwise result in compile errors may go unnoticed. Your editor or IDE can be configured to apply build tags but the settings are often buried in the user interface and found in different places.

A build tag is a condition that determines if a file should be included in a package. This is helpful when limiting Go code to a specific operating system or architecture but feels semantically misused for integration tests.

What about flags?

Flags are a great utility in Go and would have been my choice if it wasn’t for the following problem. Flags need to be defined and parsed. If a package does not define the flag an error will be returned when trying to run all tests:

go test ./... -integration
flag provided but not defined: -integration

What about short mode?

Using a built-in test flag could be ideal because they are available in every test unless you define your own test main function and forgot to parse the flags.

Unfortunately, there is no -long variant of it. Someone who clones a Go repository should be able to run go test ./... without setting up any dependencies. Using short mode assumes that everyone runs:

go test ./... -short

More so, there are unit tests which can take a long time and there are integration tests which can be very fast.

Environment Variables

Environment variables are the only option that do not have any of the mentioned problems. You can run tests without dependencies and you can run all tests by setting the variable. Your code will always be statically checked by compilers and linters without having to configure anything. Most importantly, someone else cloning your repository will not have to set up anything.

If you use integration tests across different packages, you can set up a package specifically for testing:

package testing

import (
  "os"
  "testing"
)

func IntegrationTest(t *testing.T) {
  if os.Getenv("INTEGRATION") == "" {
    t.Skip("skipping integration tests: set INTEGRATION environment variable")
  }
}

It should be easy for a developer to write an integration test. With this approach: