Skip to content

Fix: Go Test Not Working — Tests Not Running, Failing Unexpectedly, or Coverage Not Collected

FixDevs ·

Quick Answer

How to fix Go testing issues — test function naming, table-driven tests, t.Run subtests, httptest, testify assertions, and common go test flag errors.

The Problem

A test function is written but go test doesn’t run it:

// utils_test.go
func testAdd(t *testing.T) {  // Lowercase 't' — not recognized as a test
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("expected 5, got %d", result)
    }
}

Or a test fails with an unclear error:

--- FAIL: TestCalculate (0.00s)
    calc_test.go:15: expected 10, got 0

Or go test ./... reports no test files:

?   mypackage [no test files]

Or race conditions appear only sometimes:

WARNING: DATA RACE
goroutine 6 [running]:
main.counter++

Why This Happens

Go’s test runner has strict naming conventions and common pitfalls:

  • Test functions must start with Test (capital T)func TestAdd is recognized; func testAdd or func addTest is not.
  • Test file must end in _test.go — files not matching this pattern are excluded from test builds.
  • Package naming matters — use package foo_test (black-box testing) or package foo (white-box testing) intentionally. Mixing them in one file causes compilation errors.
  • t.Fatal vs t.Errort.Fatal stops the current test immediately; t.Error marks failure but continues. Using t.Fatal inside a goroutine doesn’t stop the test — use a channel or sync.WaitGroup instead.

Fix 1: Follow Test Naming Conventions

Every test function must follow Go’s naming rules:

// WRONG — won't be detected by go test
func testAdd(t *testing.T) { }        // lowercase 't'
func TestingAdd(t *testing.T) { }     // 'Testing' not 'Test'
func AddTest(t *testing.T) { }        // wrong suffix position
func TestAdd() { }                    // missing *testing.T parameter

// CORRECT — recognized by go test
func TestAdd(t *testing.T) { }         // Unit test
func BenchmarkAdd(b *testing.B) { }    // Benchmark
func FuzzAdd(f *testing.F) { }         // Fuzz test (Go 1.18+)
func TestMain(m *testing.M) { }        // Test main (setup/teardown)

// File must be named with _test.go suffix
// utils_test.go — correct
// utils.test.go — wrong (. not _)
// test_utils.go — wrong (no _test suffix)

Package declaration options:

// Black-box testing (recommended for public API testing)
// Can only access exported identifiers
package calculator_test

import (
    "testing"
    "mymodule/calculator"
)

func TestAdd(t *testing.T) {
    result := calculator.Add(2, 3)
    // ...
}

// White-box testing (access to unexported identifiers)
package calculator

func TestInternalHelper(t *testing.T) {
    result := internalHelper()  // Can access unexported functions
    // ...
}

Fix 2: Write Table-Driven Tests

Table-driven tests are the idiomatic Go pattern for testing multiple inputs:

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -1, -2, -3},
        {"zeros", 0, 0, 0},
        {"mixed", -5, 10, 5},
    }

    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            result := Add(tc.a, tc.b)
            if result != tc.expected {
                t.Errorf("Add(%d, %d) = %d, want %d", tc.a, tc.b, result, tc.expected)
            }
        })
    }
}

Running specific subtests:

# Run all tests
go test ./...

# Run a specific test function
go test -run TestAdd ./...

# Run a specific subtest
go test -run "TestAdd/positive_numbers" ./...

# Run with verbose output
go test -v -run TestAdd ./...

# Run with race detector (always use in CI)
go test -race ./...

# Run with coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out  # Open in browser

Parallel subtests for faster execution:

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"case1", 1, 2, 3},
        {"case2", 4, 5, 9},
    }

    for _, tc := range tests {
        tc := tc  // Capture range variable — required before Go 1.22
        t.Run(tc.name, func(t *testing.T) {
            t.Parallel()  // Each subtest runs in parallel
            result := Add(tc.a, tc.b)
            if result != tc.expected {
                t.Errorf("got %d, want %d", result, tc.expected)
            }
        })
    }
}

Note: In Go 1.22+, loop variables are scoped per iteration, so the tc := tc capture is no longer needed. For earlier versions, it’s required to prevent all subtests from using the final loop value.

Fix 3: Test HTTP Handlers with httptest

Use the net/http/httptest package to test HTTP handlers without starting a real server:

package handlers_test

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
)

func TestGetUserHandler(t *testing.T) {
    // Create a request
    req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
    req.Header.Set("Authorization", "Bearer test-token")

    // Create a response recorder
    rec := httptest.NewRecorder()

    // Call the handler directly
    GetUserHandler(rec, req)

    // Assert response
    if rec.Code != http.StatusOK {
        t.Errorf("expected status 200, got %d", rec.Code)
    }

    var body map[string]any
    if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
        t.Fatalf("failed to decode response: %v", err)
    }
    if body["id"] != "123" {
        t.Errorf("expected id '123', got %v", body["id"])
    }
}

// Test a full router/mux
func TestRouter(t *testing.T) {
    router := setupRouter()  // Your router setup function

    tests := []struct {
        method   string
        path     string
        body     string
        wantCode int
    }{
        {http.MethodGet, "/users", "", http.StatusOK},
        {http.MethodPost, "/users", `{"name":"Alice"}`, http.StatusCreated},
        {http.MethodGet, "/users/999", "", http.StatusNotFound},
    }

    for _, tc := range tests {
        t.Run(tc.method+" "+tc.path, func(t *testing.T) {
            var bodyReader *strings.Reader
            if tc.body != "" {
                bodyReader = strings.NewReader(tc.body)
            }
            req := httptest.NewRequest(tc.method, tc.path, bodyReader)
            if tc.body != "" {
                req.Header.Set("Content-Type", "application/json")
            }
            rec := httptest.NewRecorder()
            router.ServeHTTP(rec, req)
            if rec.Code != tc.wantCode {
                t.Errorf("expected %d, got %d", tc.wantCode, rec.Code)
            }
        })
    }
}

Fix 4: Mock Dependencies with Interfaces

Go’s interface system makes mocking straightforward without external frameworks:

// Production code — depend on an interface, not a concrete type
type UserRepository interface {
    GetUser(id string) (*User, error)
    CreateUser(user *User) error
}

type UserService struct {
    repo UserRepository
}

func (s *UserService) GetProfile(id string) (*Profile, error) {
    user, err := s.repo.GetUser(id)
    if err != nil {
        return nil, fmt.Errorf("get user: %w", err)
    }
    return &Profile{Name: user.Name, Email: user.Email}, nil
}

// Test code — implement the interface as a mock
type mockUserRepo struct {
    users map[string]*User
    err   error
}

func (m *mockUserRepo) GetUser(id string) (*User, error) {
    if m.err != nil {
        return nil, m.err
    }
    user, ok := m.users[id]
    if !ok {
        return nil, fmt.Errorf("user %s not found", id)
    }
    return user, nil
}

func (m *mockUserRepo) CreateUser(user *User) error {
    return m.err
}

// Test using the mock
func TestGetProfile(t *testing.T) {
    t.Run("success", func(t *testing.T) {
        repo := &mockUserRepo{
            users: map[string]*User{
                "1": {ID: "1", Name: "Alice", Email: "[email protected]"},
            },
        }
        svc := &UserService{repo: repo}
        profile, err := svc.GetProfile("1")
        if err != nil {
            t.Fatal(err)
        }
        if profile.Name != "Alice" {
            t.Errorf("expected Alice, got %s", profile.Name)
        }
    })

    t.Run("user not found", func(t *testing.T) {
        repo := &mockUserRepo{err: fmt.Errorf("not found")}
        svc := &UserService{repo: repo}
        _, err := svc.GetProfile("999")
        if err == nil {
            t.Fatal("expected error, got nil")
        }
    })
}

Fix 5: Setup and Teardown Patterns

Handle test setup, teardown, and shared state with TestMain and helper functions:

package db_test

import (
    "os"
    "testing"
)

var testDB *sql.DB

// TestMain runs before any test in the package
func TestMain(m *testing.M) {
    // Setup
    var err error
    testDB, err = sql.Open("postgres", os.Getenv("TEST_DATABASE_URL"))
    if err != nil {
        log.Fatalf("failed to open test database: %v", err)
    }

    if err := runMigrations(testDB); err != nil {
        log.Fatalf("failed to run migrations: %v", err)
    }

    // Run tests
    exitCode := m.Run()

    // Teardown
    testDB.Close()
    os.Exit(exitCode)
}

// Per-test cleanup using t.Cleanup
func TestCreateUser(t *testing.T) {
    // Insert test data
    userID := insertTestUser(t, testDB)

    // t.Cleanup runs after the test (and subtests) finish
    t.Cleanup(func() {
        testDB.Exec("DELETE FROM users WHERE id = $1", userID)
    })

    // Test logic
    user, err := GetUser(testDB, userID)
    if err != nil {
        t.Fatal(err)
    }
    if user.ID != userID {
        t.Errorf("expected ID %d, got %d", userID, user.ID)
    }
}

// Skip tests that need external dependencies
func TestWithExternalService(t *testing.T) {
    if os.Getenv("INTEGRATION_TESTS") == "" {
        t.Skip("skipping integration test; set INTEGRATION_TESTS=1 to run")
    }
    // ... integration test code
}

Fix 6: Use testify for Cleaner Assertions

The testify library reduces boilerplate and produces clearer failure messages:

go get github.com/stretchr/testify
import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    "github.com/stretchr/testify/mock"
)

func TestUserService(t *testing.T) {
    user := &User{ID: "1", Name: "Alice", Email: "[email protected]"}

    // assert — continues test on failure
    assert.Equal(t, "Alice", user.Name)
    assert.NotNil(t, user)
    assert.Contains(t, user.Email, "@")
    assert.Len(t, user.Name, 5)

    // require — stops test on failure (like t.Fatal)
    // Use require when subsequent assertions depend on this one
    require.NotNil(t, user, "user must not be nil")
    require.Equal(t, "1", user.ID)  // If this fails, test stops here

    // Error assertions
    err := validateUser(user)
    assert.NoError(t, err)

    _, err = GetUser("nonexistent")
    assert.Error(t, err)
    assert.ErrorContains(t, err, "not found")
}

// testify/mock — generate mock implementations
type MockUserRepo struct {
    mock.Mock
}

func (m *MockUserRepo) GetUser(id string) (*User, error) {
    args := m.Called(id)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*User), args.Error(1)
}

func TestWithMock(t *testing.T) {
    mockRepo := new(MockUserRepo)

    // Set expectation
    mockRepo.On("GetUser", "1").Return(&User{ID: "1", Name: "Alice"}, nil)
    mockRepo.On("GetUser", "999").Return(nil, errors.New("not found"))

    svc := &UserService{repo: mockRepo}

    user, err := svc.GetProfile("1")
    require.NoError(t, err)
    assert.Equal(t, "Alice", user.Name)

    // Verify all expected calls were made
    mockRepo.AssertExpectations(t)
}

Still Not Working?

Tests pass locally but fail in CI — the most common causes are timing dependencies, environment variable differences, and hardcoded file paths. Use t.TempDir() for temp files (auto-cleaned), os.Getenv for config, and avoid time.Sleep in tests — use channels or require.Eventually from testify instead.

t.Fatal inside a goroutine panics — calling t.Fatal or t.FailNow from a goroutine other than the test goroutine causes a panic. Use t.Error (which is goroutine-safe) and coordinate with a WaitGroup or channel:

func TestConcurrent(t *testing.T) {
    var wg sync.WaitGroup
    errCh := make(chan error, 1)

    wg.Add(1)
    go func() {
        defer wg.Done()
        if err := doWork(); err != nil {
            errCh <- err  // Don't call t.Fatal here
        }
    }()

    wg.Wait()
    close(errCh)

    if err := <-errCh; err != nil {
        t.Fatal(err)  // Call t.Fatal from the test goroutine
    }
}

Coverage not including all packagesgo test -cover ./... only covers packages that have tests. To measure coverage for packages without test files, use -coverpkg=./...:

go test -coverpkg=./... -coverprofile=coverage.out ./...

Build constraints exclude test files — if a test file has a //go:build constraint at the top (like //go:build integration), it won’t run unless you pass the tag: go test -tags integration ./....

For related Go issues, see Fix: Go Goroutine Deadlock and Fix: Go Channel Deadlock.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles