Skip to content

Chapter 29: Setup/Teardown Fixtures

Description

Use TestMain for package-level setup/teardown, struct-based fixtures for per-test state, t.TempDir() for temporary files, and os.Setenv/os.Unsetenv with defer for environment variable mocking. Fixtures isolate test state and guarantee cleanup regardless of test outcome.

Code

type Fixture struct {
    DB *Database
}

func NewFixture() *Fixture {
    return &Fixture{DB: NewDatabase()}
}

func (f *Fixture) Setup() {
    f.DB.Insert("config", map[string]interface{}{"key": "version", "value": "1.0"})
}

func (f *Fixture) Teardown() {
    f.DB.Close()
}

type EnvFixture struct {
    original map[string]string
}

func NewEnvFixture() *EnvFixture {
    return &EnvFixture{original: make(map[string]string)}
}

func (ef *EnvFixture) Set(key, value string) {
    ef.original[key] = os.Getenv(key)
    os.Setenv(key, value)
}

func (ef *EnvFixture) Restore() {
    for key, val := range ef.original {
        if val == "" { os.Unsetenv(key) } else { os.Setenv(key, val) }
    }
}

Test

func TestFixture_SetupTeardown(t *testing.T) {
    f := NewFixture()
    f.Setup()
    rows, err := f.DB.Query("config")
    require.NoError(t, err)
    assert.Equal(t, "1.0", rows[0]["value"])
    f.Teardown()
    assert.Equal(t, 0, f.DB.Count("config"))
}

func TestResourceManager(t *testing.T) {
    t.Run("create and cleanup", func(t *testing.T) {
        rm := NewResourceManager()
        path, err := rm.CreateTempFile("hello world")
        require.NoError(t, err)
        data, _ := os.ReadFile(path)
        assert.Equal(t, "hello world", string(data))
        rm.Cleanup()
        _, err = os.Stat(path)
        assert.True(t, os.IsNotExist(err))
    })
}

func TestEnvFixture(t *testing.T) {
    ef := NewEnvFixture()
    ef.Set("MY_APP_MODE", "test")
    assert.Equal(t, "test", os.Getenv("MY_APP_MODE"))
    ef.Restore()
    assert.Equal(t, "", os.Getenv("MY_APP_MODE"))
}

func TestMustGetenv(t *testing.T) {
    os.Setenv("TEST_VAR", "custom")
    defer os.Unsetenv("TEST_VAR")
    assert.Equal(t, "custom", MustGetenv("TEST_VAR", "default"))
    assert.Equal(t, "default", MustGetenv("NONEXISTENT", "default"))
}

func TestDatabase_InsertAndQuery(t *testing.T) {
    db := NewDatabase()
    db.Insert("users", map[string]interface{}{"name": "Alice"})
    rows, _ := db.Query("users")
    assert.Len(t, rows, 1)
}

func TestDatabase_Truncate(t *testing.T) {
    db := NewDatabase()
    db.Insert("temp", map[string]interface{}{"id": 1})
    db.Truncate("temp")
    assert.Equal(t, 0, db.Count("temp"))
}

Testing Approach

Setup/teardown fixtures:

  1. Fixture struct per testNewFixture() returns a fresh Fixture with its own Database. Each test sets up, runs, and tears down. No shared state means no pollution between tests.
  2. defer for teardownf.Teardown() in defer guarantees cleanup even if the test panics. EnvFixture.Restore() restores environment variables regardless of test outcome.
  3. TestMain for expensive resourcesTestMain creates a TestSuite, runs Setup() once, runs all tests via m.Run(), then runs Teardown(). Use sparingly — one-time setup is for resources that are expensive to create (database connections, test servers).
  4. os.Setenv + defer os.Unsetenv — env var changes are global. Always pair Setenv with defer Unsetenv in the same test. EnvFixture provides a reusable wrapper that saves originals and restores them.

View source code on GitHub