Question

Test Setup in Go with TestMain and t.Cleanup

gounit-testing

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 call m.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:

  • TestMain opens 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.Cleanup clears 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.

Take Quiz

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:

  1. The Go test runner finds TestMain for the greeting package.
  2. TestMain sets the shared prefix value to "Hello".
  3. m.Run() starts the package’s tests.
  4. TestGreeting calls setupTest.
  5. setupTest creates test-specific data and registers cleanup with t.Cleanup.
  6. The test builds and checks the greeting.
  7. When TestGreeting ends, its cleanup function runs and clears name.
  8. After every package test has finished, m.Run() returns its result code.
  9. TestMain clears the package-wide state and passes the result code to os.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.Server for 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.
}
Take Quiz

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

NeedGo approachScope
Setup before every testCall a helper from each testOne test
Cleanup after every testt.Cleanup(func() { ... })One test
Setup once for a packageTestMain(m *testing.M) before m.Run()All tests in one package
Teardown once for a packageCode after m.Run() in TestMainAll tests in one package
Temporary test directoryt.TempDir()One test
A group with shared setupA subtest helper or explicit setup around t.RunA 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.

TechniqueAdvantagesTrade-off
TestMainGood for expensive package-wide resourcesShared state can make tests less isolated
Setup helperExplicit dependencies and independent testsMust be called by each relevant test
t.CleanupCleanup runs with the test lifecycleOnly cleans resources registered by that test
Global variablesEasy to accessOften causes ordering and parallel-test problems
Take Quiz

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 TestMain in a *_test.go file.
  • There can be one TestMain per tested package.
  • Call m.Run() exactly once in normal usage.
  • Run cleanup after m.Run() and before os.Exit(code).
  • Use t.Helper() in test helpers.
  • Register t.Cleanup immediately 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.

Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more