skip to content

Tagging Slow Suites

A //go:build integration line keeps expensive tests out of the default run until you pass -tags, while testing.Short() and -short give a cheaper in-file switch. Interviewers ask which you choose.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

How do you put a Go integration test file behind a build tag so it runs only on demand?

level: middleimportance: must knowfreq 55%

answer

  1. first line of the file, above package
  2. the blank line is load-bearing
  3. opt in on the command line
  4. //go:build name plus go test -tags=name

basics

~20 s

Put //go:build integration on the first line of the _test.go file, followed by a blank line before the package clause. The file is then excluded from every ordinary go test run and compiled back in only by go test -tags=integration.

solid answer

~50 s

Give the slow file its own constraint: `//go:build integration` as the first line, then a blank line, then `package migrate_test`. Any `go test ./...` without that tag drops the file from the package before compiling — if it was the package's only test file you get a `[no test files]` line — and `go test -tags=integration ./...` compiles it back in and runs it. Two mechanical details decide whether it works: the constraint has to sit above the package clause with a blank line after it, or it is just an ordinary comment and the slow tests run on every build; and `-tags` takes a comma-separated list, so `-tags=integration,slow` combines them. Because an excluded file is never compiled, nothing type-checks it in the default run — that is the real cost, and the reason a tagged build has to run somewhere on a schedule.

code

go · 9 lines
go
//go:build integration

package migrate_test

import "testing"

func TestApplyMigrationsAgainstRealDB(t *testing.T) {
	// compiled only under: go test -tags=integration ./...
}

go deeper

for a junior

Know the shape by heart: //go:build integration, a blank line, then the package clause — and that the tests come back with go test -tags=integration ./...

for a middle

Explain that the constraint is evaluated when the package's file list is assembled, so the excluded file is never compiled, and that constraints apply per file rather than per test function.

for a senior

Talk about the consequence you have to manage: the excluded suite is not type-checked by the default run, so schedule a tagged build or at least a tagged vet, and make its result as visible as the pre-merge one.

for a principal

Decide which suites earn a tag at all, and who is accountable when a tagged suite drifts out of compilation for a month with nobody noticing.

## The shape A slow suite that needs a real database is put behind an opt-in constraint like this: ```go //go:build integration package migrate_test import "testing" func TestApplyMigrationsAgainstRealDB(t *testing.T) { // only compiled when the integration tag is supplied } ``` The first line is the constraint. The blank line after it is not decoration: a `//go:build` comment counts as a build constraint only when it appears before the package clause and is followed by a blank line. Without the blank line it becomes a doc comment on the package, the constraint disappears, and the file is compiled into every build — the exact opposite of what you wanted, and a mistake that is easy to miss because everything still passes, just slowly. ## What the go command does with it Build constraints are evaluated while the go command is deciding which files make up the package, long before anything is compiled. If the constraint is not satisfied, the file is not parsed, not type-checked, not compiled and not linked. This has three consequences worth stating out loud: 1. Excluding a suite this way is free at run time — there is no test to skip, because there is no test. 2. Nothing reports it. An excluded test cannot print a SKIP line, so an ordinary run gives you no hint that the suite exists. 3. Errors inside the file are invisible in the default build. A rename in the package under test can leave the tagged file uncompilable for weeks while the pipeline stays green. ## Opting in `go test -tags=integration ./...` puts the tag into the build context and the file comes back. The flag takes a comma-separated list — `-tags=integration,slow` — and the same flag exists on `go build`, `go vet` and `go list`, which matters when you want to inspect or lint the tagged configuration rather than run it. An unrecognised tag is not an error. `-tags=integraton` (misspelled) satisfies nothing, the file stays excluded, and the command exits 0. The failure mode of build tags is always silence. ## Granularity: per file, never per function A build constraint applies to a whole file. There is no way to constrain a single test function, so gating a suite means physically splitting the package's tests: the fast ones stay in `migrate_test.go`, the slow ones move to `migrate_integration_test.go` with the constraint on top. When splitting is awkward — for instance, a handful of table entries inside one test are the slow part — a `testing.Short()` guard is the better mechanism, because it works per test and leaves the file in the build. ## Choosing a tag name Tag names are arbitrary identifiers; `integration` is only a convention, not something the toolchain knows. Because they are arbitrary, they are also unchecked, which argues for keeping the set small, writing it down, and having exactly one command per tag in the pipeline so nobody has to guess. Prefer a name that describes what the suite *needs* rather than how long it takes: what changes over time is the run time, not the dependency on external infrastructure. ## The operational half Adding the tag is one line; keeping the suite alive is the actual work. Since the default build does not compile the file, the tagged configuration needs a scheduled run — and the result of that run has to be treated as a real signal, with an owner, rather than a job whose red state is background noise. A cheap intermediate step is running `go vet -tags=integration ./...` or `go build -tags=integration ./...` on every push: it type-checks the excluded files in seconds without paying for the suite itself, and catches the rot that silence would otherwise hide.

  • What happens if you forget the blank line between //go:build and the package clause?
    The line stops being a build constraint and becomes an ordinary comment on the package, so the file is compiled into every build and the slow tests run on every go test. Nothing fails, it just gets slow. go vet's build-tag check reports a misplaced //go:build comment, and go test runs that check by default, so it usually surfaces quickly.
  • Is the tagged file excluded at build time or at run time?
    At build time. The go command evaluates the constraint while assembling the package's file list, so an excluded file is never parsed, type-checked, compiled or linked. That is why it costs nothing at run time, and also why a compile error inside it goes unnoticed until somebody builds with the tag.
  • Where do you put the constraint if only some tests in the package are slow?
    You split the file, because a build constraint is per file and never per function. The slow tests move into their own _test.go with the constraint on top; the fast ones stay in the untagged file. If splitting is awkward, use a testing.Short() guard instead, which is per test and keeps everything compiled.
  • Does passing a tag nobody declared cause an error?
    No. Tag names are arbitrary identifiers, so -tags=integraton with a typo simply satisfies no constraint: the file stays out of the build, the run exits 0, and nothing warns you. Every failure mode of build tags is silence, which is why the tagged configuration needs its own scheduled build.

A build tag is the light switch on the wall of the room the code lives in. Flick it off and you don't get a dim room — you get no room at all, because the file is not in the building.

saying these in an interview costs you the question

  • Puts the //go:build line below the package clause
  • Omits the blank line, leaving the constraint as a comment
  • Thinks -tags filters tests at run time
  • Expects a build tag to select individual test functions
  • Assumes go test warns when every test file is excluded
open as a page

What does the go test -short flag actually do, and how does a slow test opt out under it?

level: juniorimportance: should knowfreq 46%

basics

~10 s

The -short flag only sets a boolean that testing.Short() reports; it skips nothing on its own. Each slow test has to check testing.Short() itself and call t.Skip so the fast run passes over it.

open as a page

A Go tool's -tags=integration suite was green for weeks without running. How do you prove that?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Ask the go command which files it actually built: go list -f '{{.TestGoFiles}}' shows the tagged file missing, and go test prints a no-test-files line instead of running anything. The usual causes are a misspelled build tag or a pipeline that never passes -tags.

open as a page

How do you decide which Go test suites gate a merge and which sit behind a build tag?

level: principalimportance: should knowfreq 26%

basics

~20 s

Gate merges on suites whose failure the diff explains and a developer can diagnose in minutes; push the rest behind a build tag or a testing.Short guard. The decisive cost is that a tagged file is not even compiled by the default run.

open as a page

When is a _linux_test.go filename better than skipping the test with a runtime.GOOS check?

level: middleimportance: nice to knowfreq 27%

basics

~20 s

Use the _linux suffix when the file only compiles on Linux — the name alone keeps it out of every other build. Use a runtime.GOOS check with t.Skip when the file compiles everywhere and only the behaviour is Linux-specific.

open as a page