Fix: Go Test Not Working — Tests Not Running, Failing Unexpectedly, or Coverage Not Collected
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 0Or 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 TestAddis recognized;func testAddorfunc addTestis 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) orpackage foo(white-box testing) intentionally. Mixing them in one file causes compilation errors. t.Fatalvst.Error—t.Fatalstops the current test immediately;t.Errormarks failure but continues. Usingt.Fatalinside a goroutine doesn’t stop the test — use a channel orsync.WaitGroupinstead.
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 browserParallel 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 := tccapture 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/testifyimport (
"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 packages — go 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Go Generics Type Constraint Error — Does Not Implement or Cannot Use as Type
How to fix Go generics errors — type constraints, interface vs constraint, comparable, union types, type inference failures, and common generic function pitfalls.
Fix: Go Error Handling Not Working — errors.Is, errors.As, and Wrapping
How to fix Go error handling — errors.Is vs ==, errors.As for type extraction, fmt.Errorf %w for wrapping, sentinel errors, custom error types, and stack traces.
Fix: Go Panic Not Recovered — panic/recover Patterns and Common Pitfalls
How to handle Go panics correctly — recover() placement, goroutine panics, HTTP middleware recovery, defer ordering, distinguishing panics from errors, and when not to use recover.
Fix: Supertest Not Working — Requests Not Sending, Server Not Closing, or Assertions Failing
How to fix Supertest HTTP testing issues — Express and Fastify setup, async test patterns, authentication headers, file uploads, JSON body assertions, and Vitest/Jest integration.