Question
How can I perform test setup that prepares shared state or test data when using Go’s built-in testing package?
For example, NUnit provides a [SetUp] attribute that runs initialization code before each test:
[TestFixture]
public class SuccessTests
{
[SetUp]
public void Init()
{
// Load test data
}
}
What is the equivalent approach in Go, including setup that runs once for the entire test package and setup that runs before individual tests?
Short Answer
Go does not use setup attributes. Instead, use TestMain for package-wide setup and teardown, and call helper functions from each test for per-test setup. Use t.Cleanup to reliably release resources after a test finishes.
Concept
Go tests are ordinary Go functions, so setup is explicit rather than controlled by annotations or attributes. This makes it easy to see where test data, temporary files, database connections, and configuration are created.
There are two common setup scopes:
- Package-wide setup: Runs once before the tests in one package. Define
func TestMain(m *testing.M)and callm.Run(). - Per-test setup: Runs before every individual test. Create a helper function and call it at the start of each test.
TestMain is useful for expensive shared resources, such as starting a local test server or preparing a test database. Per-test setup is useful when every test needs clean, independent state.
Go also provides t.Cleanup, which registers cleanup work to run when the current test ends. It is safer than relying only on defer inside helper functions because the cleanup is attached directly to the test lifecycle, including when a test fails with t.Fatal.
Mental Model
Think of a test package as a workshop:
TestMainopens the workshop once: turn on the lights, prepare shared tools, and then let all tests run.- A per-test setup helper prepares one workbench: place clean materials on it before one test starts.
t.Cleanupclears that workbench after the test, even if the test stops early.
Do not put mutable, shared materials on every workbench unless tests are designed to coordinate. Independent test data makes tests reliable and safe to run in any order.
Syntax and Examples
Package-wide setup uses TestMain in a file ending in _test.go:
package calculator
import (
"os"
"testing"
)
func TestMain(m *testing.M) {
// Setup that runs once for this test package.
loadSharedTestData()
exitCode := m.Run()
// Teardown that runs once after all tests finish.
clearSharedTestData()
os.Exit(exitCode)
}
func loadSharedTestData() {
// Load configuration, start a test service, and so on.
}
func clearSharedTestData() {
// Stop services or remove shared resources.
}
A per-test setup helper accepts *testing.T. Mark it with t.Helper() so failures are reported at the test call site rather than inside setup code.
package calculator
import "testing"
type User struct {
Name string
Admin bool
}
func newTestUser(t *testing.T) User {
t.Helper()
user := User{Name: "Ava", Admin: false}
t.Cleanup(func() {
// Remove any resource created for this specific test.
})
return user
}
func TestRegularUserIsNotAdmin(t *testing.T) {
user := newTestUser(t)
if user.Admin {
t.Fatal("expected a regular user")
}
}
Call the helper at the start of each test that needs it. This is the usual Go equivalent of NUnit’s per-test [SetUp].
Step by Step Execution
Consider this complete example:
package greeting
import (
"os"
"testing"
)
var prefix string
func TestMain(m *testing.M) {
prefix = "Hello"
code := m.Run()
prefix = ""
os.Exit(code)
}
func setupTest(t *testing.T) string {
t.Helper()
name := "Mina"
t.Cleanup(func() {
name = ""
})
return name
}
func TestGreeting(t *testing.T) {
name := setupTest(t)
got := prefix + ", " + name
want := "Hello, Mina"
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
}
Execution order:
- The Go test runner finds
TestMainfor thegreetingpackage. TestMainsets the sharedprefixvalue to"Hello".m.Run()starts the package’s tests.TestGreetingcallssetupTest.setupTestcreates test-specific data and registers cleanup witht.Cleanup.- The test builds and checks the greeting.
- When
TestGreetingends, its cleanup function runs and clearsname. - After every package test has finished,
m.Run()returns its result code. TestMainclears the package-wide state and passes the result code toos.Exit.
The key point is that code after m.Run() runs only after all tests in that package have completed.
Real World Use Cases
Common uses for package-wide setup with TestMain include:
- Loading an environment configuration used by all tests in a package.
- Starting a local HTTP server or test dependency once.
- Creating a temporary directory shared by read-only fixture tests.
- Connecting to a dedicated test database when the suite is intentionally integration-based.
Common uses for per-test setup helpers include:
- Creating a new user, order, or account for one test.
- Making a temporary directory with
t.TempDir(). - Starting an
httptest.Serverfor a test. - Building a fresh in-memory repository or cache.
- Adding records that must not affect another test.
For example, an HTTP server belongs naturally to one test:
func TestClientGetsProfile(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"name":"Ava"}`))
}))
t.Cleanup(server.Close)
// Create a client that uses server.URL and test it.
}
Real Codebase Usage
In production codebases, test setup is often organized around small helpers rather than one large fixture object.
Prefer fresh state for unit tests
A typical unit test creates a new dependency for every test:
func newTestStore(t *testing.T) *Store {
t.Helper()
return NewStore()
}
func TestStoreSavesItem(t *testing.T) {
store := newTestStore(t)
store.Save("theme", "dark")
got, ok := store.Get("theme")
if !ok || got != "dark" {
t.Fatal("expected saved item")
}
}
Fresh state prevents a test from passing or failing because another test ran first.
Use t.TempDir for files
func TestConfigIsWritten(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.json")
err := os.WriteFile(path, []byte(`{"enabled":true}`), 0o600)
if err != nil {
t.Fatal(err)
}
}
The testing package removes the directory automatically after the test.
Use cleanup close to resource creation
func openTestFile(t *testing.T) *os.File {
t.Helper()
file, err := os.CreateTemp(t.TempDir(), "report-*.txt")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = file.Close() })
return file
}
Keep TestMain small
Use TestMain for true package-level concerns. Avoid using it as a hidden place for every test’s data. Tests should still clearly create the inputs they depend on.
Common Mistakes
Forgetting to call m.Run()
Without m.Run(), no regular tests execute.
func TestMain(m *testing.M) {
prepare()
// Wrong: tests are never run.
}
Call m.Run() and preserve its exit code:
func TestMain(m *testing.M) {
prepare()
code := m.Run()
cleanup()
os.Exit(code)
}
Using defer os.Exit(...) in TestMain
os.Exit immediately terminates the process and does not run deferred functions. Perform cleanup before calling it.
func TestMain(m *testing.M) {
prepare()
code := m.Run()
cleanup()
os.Exit(code)
}
Sharing mutable global state between tests
var users []string
func TestAddUser(t *testing.T) {
users = append(users, "Ava")
}
Another test may observe the modified slice. Prefer a new slice, store, or database transaction per test.
Omitting t.Helper()
Without t.Helper(), an error reported by a helper can point at the helper implementation instead of the test that called it. Put t.Helper() near the beginning of reusable test helpers.
Cleaning up only on the happy path
If cleanup is placed after an assertion, a t.Fatal can prevent it from being reached. Register cleanup immediately after creating the resource:
server := httptest.NewServer(handler)
t.Cleanup(server.Close)
Calling t.Parallel() with shared setup state
Parallel tests must not modify the same global variables, files, database rows, or other resources without isolation. Give each parallel test its own data, such as a t.TempDir() directory or unique database record.
Comparisons
| Need | Go approach | Scope |
|---|---|---|
| Setup before every test | Call a helper from each test | One test |
| Cleanup after every test | t.Cleanup(func() { ... }) | One test |
| Setup once for a package | TestMain(m *testing.M) before m.Run() | All tests in one package |
| Teardown once for a package | Code after m.Run() in TestMain | All tests in one package |
| Temporary test directory | t.TempDir() | One test |
| A group with shared setup | A subtest helper or explicit setup around t.Run | A test group |
NUnit’s [SetUp] runs before each test. The closest idiomatic Go equivalent is a setup helper called by each test.
NUnit’s one-time fixture setup is closer to Go’s TestMain, but TestMain applies to the whole Go package rather than a class-based test fixture.
| Technique | Advantages | Trade-off |
|---|---|---|
TestMain | Good for expensive package-wide resources | Shared state can make tests less isolated |
| Setup helper | Explicit dependencies and independent tests | Must be called by each relevant test |
t.Cleanup | Cleanup runs with the test lifecycle | Only cleans resources registered by that test |
| Global variables | Easy to access | Often causes ordering and parallel-test problems |
Cheat Sheet
// Package-wide setup and teardown.
func TestMain(m *testing.M) {
setupPackage()
code := m.Run()
teardownPackage()
os.Exit(code)
}
// Per-test setup helper.
func setup(t *testing.T) *Resource {
t.Helper()
resource := newResource()
t.Cleanup(func() {
resource.Close()
})
return resource
}
func TestSomething(t *testing.T) {
resource := setup(t)
_ = resource
}
- Put
TestMainin a*_test.gofile. - There can be one
TestMainper tested package. - Call
m.Run()exactly once in normal usage. - Run cleanup after
m.Run()and beforeos.Exit(code). - Use
t.Helper()in test helpers. - Register
t.Cleanupimmediately after acquiring a resource. - Prefer fresh test state over mutable package globals.
- Use
t.TempDir()for automatically cleaned temporary directories. - Keep tests safe to run in any order.
FAQ
Is there a [SetUp] attribute in Go tests?
No. Go’s built-in testing package has no setup attributes. Create a helper function and call it at the start of each test that needs setup.
How do I run setup once before all Go tests?
Define func TestMain(m *testing.M) in a _test.go file. Put setup before m.Run().
How do I run teardown after all Go tests finish?
Store the result of m.Run(), run teardown code, then call os.Exit(code).
Should every test use TestMain?
No. Most unit tests only need local setup helpers. Use TestMain when a resource genuinely must be initialized once per package.
Is t.Cleanup better than defer in tests?
For resources created by a test helper, t.Cleanup is usually preferable because cleanup is registered on the test itself. defer is still valid for simple cleanup within the same function.
Can TestMain return an error?
No. TestMain has a fixed signature: func TestMain(m *testing.M). Report setup failures using logging or by exiting with a non-zero status after appropriate cleanup.
Does TestMain run once for the entire repository?
No. It runs once for the test process of its package. A repository with multiple packages can have separate test processes and separate TestMain functions.
Can I use t.Parallel() with package-wide setup?
Yes, but package-wide data must be safe for concurrent access and should not be mutated by parallel tests. Prefer per-test isolated resources.
Mini Project
Description
Build a small file-backed settings loader test suite. Each test receives a clean temporary directory and creates its own settings file. This demonstrates per-test setup, automatic cleanup, and independent test data without relying on shared global files.
Goal
Test that a settings loader reads a valid file and returns an error when the file is missing.
Requirements
Create a LoadTheme function that reads a file containing a theme name.
Create a test helper that makes a temporary directory for each test.
Write one test for a valid theme.txt file containing dark.
Write one test for a missing file.
Use t.Helper() in the setup helper and t.TempDir() for cleanup.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.