Project ⏱ 6 min read

12 · Testing

Unit tests with mocks, table-driven tests, coverage

Test File Convention

Test files live next to the code they test, suffixed with _test.go:

internal/services/
├── coupon_service.go
└── coupon_service_test.go   ← same package: "package services"

Same package means tests can access unexported functions — useful for testing internal helpers.

Table-Driven Tests — Real Example

From internal/services/coupon_service_test.go:

func TestGenerateCouponInternalName(t *testing.T) {
  tests := []struct {
    input    string
    expected string
  }{
    {"Welcome Voucher 100 Baht", "WELCOME_VOUCHER_100_BAHT"},
    {"simple",                   "SIMPLE"},
    {"Already UPPER",            "ALREADY_UPPER"},
    {"  trimmed  ",              "TRIMMED"},
    {"",                         ""},
    {"a b",                      "A_B"},
  }

  for _, tt := range tests {
    result := generateCouponInternalName(tt.input)
    assert.Equal(t, tt.expected, result, "input: %q", tt.input)
  }
}

Add a name string field to the struct to get better failure messages: t.Run(tt.name, func(t *testing.T) { ... })

Mocking Repository Interfaces

Services accept interfaces, so tests inject a mock instead of a real DB connection:

// Mock generated by testify/mock — lives in same package as tests
type MockCouponRepository struct {
  mock.Mock
}

func (m *MockCouponRepository) GetByID(ctx context.Context, id string) (*models.Coupon, error) {
  args := m.Called(ctx, id)
  if args.Get(0) == nil {
    return nil, args.Error(1)
  }
  return args.Get(0).(*models.Coupon), args.Error(1)
}

// In a test:
func TestGetCouponByID_Success(t *testing.T) {
  mockRepo := new(MockCouponRepository)
  mockUserRepo := new(MockUserCouponRepository)
  mockCountRepo := new(MockUserCouponsCountRepository)
  mockLogger := new(MockLogger)

  coupon := &models.Coupon{ID: "abc", Name: "10% Off"}
  mockRepo.On("GetByID", mock.Anything, "abc").Return(coupon, nil)

  svc := NewCouponService(mockLogger, mockRepo, mockUserRepo, mockCountRepo)
  result, err := svc.GetCouponByID(context.Background(), "abc")

  assert.NoError(t, err)
  assert.Equal(t, "10% Off", result.Name)
  mockRepo.AssertExpectations(t)
}

Run Tests

# All tests
make test

# Single package
go test -v ./internal/services/...

# Single test function
go test -v -run TestGetCouponByID ./internal/services/...

# With race detector
go test -race ./...

# Coverage report
make test-coverage
open coverage.html

Integration Tests

Integration tests live in tests/ and use miniredis (in-memory Redis) and go-sqlmock (mock SQL driver) to test the full stack without real infrastructure.

// tests/ use build tags to separate from unit tests
// Run integration tests:
go test -v -tags integration ./tests/...

Key Takeaways