Go Testing in 2026: Unit Tests, Mocks and Technical Interview Questions

Master Go testing with the standard library, table-driven tests, mocks, and the patterns interviewers expect. Practical examples with testify, gomock, and the testing package.

Go testing unit tests and mocks for technical interviews

Go testing relies on the standard testing package, which ships with every Go installation and requires no external dependencies. Unlike frameworks in other languages, Go's approach favors simplicity: test files live alongside production code, test functions follow a naming convention, and the go test command handles discovery and execution.

What interviewers look for

Candidates who write table-driven tests, use interfaces for dependency injection, and understand when mocking adds value versus when it adds noise.

Writing Unit Tests with the testing Package

Every test file ends with _test.go and lives in the same package as the code it tests. Test functions start with Test followed by a capitalized name. The *testing.T parameter provides methods for reporting failures.

calculator_test.gogo
package calculator

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

Running go test ./... executes all tests in the current module. The -v flag shows individual test names and their pass/fail status.

Table-Driven Tests: The Go Standard

Table-driven tests reduce duplication and make adding new cases trivial. Each row in the table represents one scenario with inputs and expected outputs. This pattern appears in the Go standard library and in production codebases across the industry.

validator_test.gogo
package validator

import "testing"

func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name    string
        email   string
        wantErr bool
    }{
        {"valid email", "user@example.com", false},
        {"missing at sign", "userexample.com", true},
        {"empty string", "", true},
        {"unicode local part", "user@例え.jp", false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateEmail(tt.email)
            if (err != nil) != tt.wantErr {
                t.Errorf("ValidateEmail(%q) error = %v, wantErr %v",
                    tt.email, err, tt.wantErr)
            }
        })
    }
}

The t.Run method creates subtests that run independently. Failed subtests report which specific case failed without stopping other cases.

Dependency Injection Through Interfaces

Go interfaces enable testing code that depends on external systems: databases, HTTP clients, file systems. Defining a minimal interface at the point of use allows swapping real implementations for test doubles.

service.gogo
package order

// Repository defines the data access methods this service needs
type Repository interface {
    FindByID(id string) (*Order, error)
    Save(order *Order) error
}

// Service handles order business logic
type Service struct {
    repo Repository
}

// NewService creates a service with the given repository
func NewService(repo Repository) *Service {
    return &Service{repo: repo}
}

// Process validates and saves an order
func (s *Service) Process(order *Order) error {
    if order.Total <= 0 {
        return ErrInvalidTotal
    }
    return s.repo.Save(order)
}

The Service accepts any type satisfying Repository. Production code passes a database-backed implementation. Tests pass a mock or stub.

Mocking with gomock and mockgen

The gomock package generates mock implementations from interface definitions. The mockgen command reads source files and produces mock structs that record calls and return configured values.

bash
# Generate mocks for the Repository interface
mockgen -source=service.go -destination=mocks/repository_mock.go -package=mocks

Using the generated mock in tests:

service_test.gogo
package order

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "go.uber.org/mock/gomock"

    "yourmodule/order/mocks"
)

func TestService_Process_SavesValidOrder(t *testing.T) {
    ctrl := gomock.NewController(t)
    mockRepo := mocks.NewMockRepository(ctrl)

    order := &Order{ID: "123", Total: 99.99}

    // Expect Save to be called once with this order, return nil
    mockRepo.EXPECT().
        Save(order).
        Return(nil).
        Times(1)

    svc := NewService(mockRepo)
    err := svc.Process(order)

    assert.NoError(t, err)
}

func TestService_Process_RejectsZeroTotal(t *testing.T) {
    ctrl := gomock.NewController(t)
    mockRepo := mocks.NewMockRepository(ctrl)

    // No calls expected to repo since validation fails first

    svc := NewService(mockRepo)
    err := svc.Process(&Order{ID: "456", Total: 0})

    assert.ErrorIs(t, err, ErrInvalidTotal)
}

The mock verifies that Save was called exactly once with the expected argument. If the code under test never calls Save, or calls it with different arguments, the test fails.

Using testify for Assertions and Suites

testify provides assertion functions that produce clearer failure messages than manual if-checks. The assert package continues execution after failures. The require package stops the test immediately.

user_test.gogo
package user

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestParseUser(t *testing.T) {
    input := `{"id": "u1", "name": "Alice"}`

    user, err := ParseUser(input)

    require.NoError(t, err, "parsing should not fail")
    assert.Equal(t, "u1", user.ID)
    assert.Equal(t, "Alice", user.Name)
    assert.Empty(t, user.Email, "email should default to empty")
}

The require.NoError call stops the test if parsing fails, preventing nil pointer panics on subsequent assertions.

Ready to ace your Go interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Testing HTTP Handlers with httptest

The net/http/httptest package provides ResponseRecorder for capturing handler output and Server for running a local test server. Most unit tests use ResponseRecorder to avoid network overhead.

handler_test.gogo
package api

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestHealthHandler(t *testing.T) {
    req := httptest.NewRequest(http.MethodGet, "/health", nil)
    rec := httptest.NewRecorder()

    HealthHandler(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code)
    assert.JSONEq(t, `{"status": "ok"}`, rec.Body.String())
}

For handlers that call external services, inject a mock client or use httptest.NewServer to create a fake backend.

Common Interview Questions on Go Testing

Interviewers assess testing knowledge through questions that reveal experience with real codebases. Here are patterns that distinguish prepared candidates:

"How would you test a function that calls an external API?"

Define an interface for the HTTP client, inject it into the function or struct, and provide a mock that returns predetermined responses. This avoids network calls and makes tests deterministic.

"When would you use a stub versus a mock?"

Stubs return canned responses without verifying how they were called. Mocks verify that specific methods were called with specific arguments. Use stubs when the return value matters more than the interaction. Use mocks when verifying the interaction is the point of the test.

"What are table-driven tests and why does Go favor them?"

Table-driven tests define test cases as data in a slice of structs. They reduce boilerplate, make adding cases trivial, and produce readable test output with t.Run. The pattern aligns with Go's preference for explicit, repetitive code over clever abstractions.

"How do you handle test fixtures or setup that multiple tests share?"

Use TestMain for package-level setup and teardown. Use helper functions or test suites (from testify) for shared setup across related tests. Avoid global state that creates coupling between tests.

Preparing for a Go interview? The Go testing module covers these patterns in depth with practice questions.

Running Tests with Coverage and Race Detection

The go test command accepts flags that reveal untested code paths and concurrency bugs.

bash
# Run tests with coverage report
go test -cover ./...

# Generate HTML coverage visualization
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html

# Detect race conditions (slower, but catches real bugs)
go test -race ./...

The race detector instruments memory accesses and flags concurrent reads and writes to the same variable. Running tests with -race in CI catches bugs that only manifest under specific timing.

Organizing Tests in Large Codebases

As projects grow, test organization affects maintainability:

  • Same package tests (package foo) access unexported functions and fields. Use for unit tests that verify internal behavior.
  • External package tests (package foo_test) import the package like external code. Use for integration tests and to verify the public API.
  • Testdata directories store fixtures like JSON files, golden outputs, or seed databases. Go ignores these during builds.
  • Build tags separate unit tests from integration tests that require external systems. Run go test -tags=integration ./... to include them.

For more on Go best practices beyond testing, the concurrency patterns article covers goroutines and channels.

What to Remember About Go Testing

  • The testing package ships with Go and requires no external dependencies
  • Table-driven tests reduce duplication and are the expected pattern in interviews
  • Interfaces enable dependency injection; define them at the point of use, not at the implementation
  • gomock generates mocks from interfaces; testify provides cleaner assertions
  • httptest.ResponseRecorder captures handler output without network overhead
  • The -race flag detects concurrency bugs that unit tests otherwise miss
  • Coverage metrics guide attention but do not guarantee correctness

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Daily challenge

Can you spot the bug in Go?

One real snippet, one hidden bug, one attempt a day. No account needed to try.

Anthony Fillion-Maillet

Written by

Anthony Fillion-Maillet

Founder of SharpSkill

Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.

Updated on August 24, 2026

Tags

#go
#testing
#unit-tests
#mocks
#interview

Share

Related articles