Go (Golang) Tutorial 0/45 lessons ~6 min read Lesson 35

    Unit Testing

    Go's testing package is built-in — no JUnit equivalent needed.

    Course progress0%
    Focus
    10 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Go's testing package is built-in — no JUnit equivalent needed. Write *_test.go files alongside source, run with go test, and use table-driven tests for comprehensive coverage. testing.T reports failures; httptest spins fake HTTP servers for handler tests.

    Mock dependencies via interfaces — generate mocks with mockery or hand-write fakes. The race detector (-race) and coverage (-cover) integrate natively. Interviewers expect table-driven tests, subtests with t.Run, and clear Arrange-Act-Assert structure.

    This lesson tests a service layer with mocked repository and an HTTP handler with httptest — production patterns used in every Go CI pipeline.

    The story

    Before merging a PR to a Kubernetes operator repo, CI runs go test ./... with table-driven cases that verify reconciliation logic for valid deployments, invalid images, and permission-denied API errors. Mock interfaces replace the real API server so tests run in milliseconds without a cluster — the workflow Google mandates for all production Go code.

    Tests live beside production code in _test.go files; go test -cover reports coverage gates that block merges below 80% on critical packages.

    Understanding the topic

    Key concepts

    • Test file: foo_test.go in same package; func TestFoo(t *testing.T).
    • Table-driven: cases := []struct{...}; for _, tc := range cases { t.Run(...) }.
    • t.Error/t.Fatal mark failure; Fatal stops test immediately.
    • httptest.NewRecorder simulates ResponseWriter.
    • Interface mocks enable isolating unit under test.
    • TestMain for global setup/teardown.
    text
    flowchart LR
    Test[go test] --> Unit[Unit Tests]
    Unit --> Bench[Benchmarks]
    Bench --> Report[Coverage Report]

    Step-by-step explanation

    1. go test ./... runs all tests recursively.
    2. go test -v verbose; -run TestName filter; -count=1 disable cache.
    3. Subtests t.Run(name, func(t){}) for isolated cases.
    4. httptest.NewRequest builds *http.Request for handler tests.
    5. testify/assert optional — stdlib sufficient for interviews.
    6. CI runs go test -race -coverprofile=coverage.out.

    Practical code example

    Table-driven service test and HTTP handler test with httptest:

    go
    package main
    import (
    "net/http"
    "net/http/httptest"
    "testing"
    )
    type Calculator struct{}
    func (Calculator) Add(a, b int) int { return a + b }
    func TestCalculator_Add(t *testing.T) {
    tests := []struct {
    name string
    a, b int
    want int
    }{
    {"positive", 2, 3, 5},
    {"zero", 0, 0, 0},
    {"negative", -1, 1, 0},
    }
    calc := Calculator{}
    for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
    got := calc.Add(tt.a, tt.b)
    if got != tt.want {
    t.Errorf("Add(%d,%d) = %d, want %d", tt.a, tt.b, got, tt.want)
    }
    })
    }
    }
    func helloHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello"))
    }
    func TestHelloHandler(t *testing.T) {
    req := httptest.NewRequest(http.MethodGet, "/hello", nil)
    rec := httptest.NewRecorder()
    helloHandler(rec, req)
    if rec.Code != http.StatusOK {
    t.Errorf("status = %d, want 200", rec.Code)
    }
    if rec.Body.String() != "hello" {
    t.Errorf("body = %q, want hello", rec.Body.String())
    }
    }

    Line-by-line code explanation

    • func TestReconcile(t *testing.T) — test functions start with Test and take *testing.T.
    • t.Run("valid deployment", func(t *testing.T) { ... }) defines subtests for table-driven cases.
    • if got != want { t.Errorf("got %v, want %v", got, want) } reports failures with context.
    • t.Fatal(err) stops the test immediately on unrecoverable setup errors.
    • httptest.NewServer(handler) spins up an in-process HTTP server for integration-style tests.
    • mockStorage := &MockStorage{...} injects fakes behind interfaces for isolated unit tests.
    • go test -race -count=1 ./... runs tests with race detection and disables result caching.
    • t.Helper() marks helper functions so failure line numbers point to the test case, not the helper.

    Key takeaway: Table-driven tests are idiomatic Go. httptest tests handlers without network. Use t.Parallel() for independent subtests when safe.

    Real-world use

    Where you'll use this in production

    • CI gate blocking merge on test failure.
    • Regression tests for bug fixes before refactor.
    • Handler contract tests without running full server.
    • Mocked service tests validating business rules.

    Best practices

    • Table-driven tests for input/output variations.
    • Test behavior not implementation details.
    • Use interfaces and mocks for external dependencies.
    • Run go test -race in CI always.
    • Name tests descriptively: TestService_Create_InvalidEmail.
    • Keep tests fast — integration tests separate package.

    Common mistakes

    • Testing private functions directly — test public API.
    • No subtests — one failure stops entire table silently.
    • Flaky tests depending on time.Now without injection.
    • Shared mutable state between tests without reset.
    • Not running -race — shipping data races to production.

    Advanced interview questions

    Q1BeginnerTest file naming?
    foo_test.go alongside foo.go; func TestXxx(t *testing.T).
    Q2BeginnerTable-driven test benefit?
    Add cases without new functions; clear input/output documentation.
    Q3IntermediateMock vs integration test?
    Mock isolates unit with fake deps; integration hits real DB/HTTP.
    Q4Intermediatehttptest usage?
    NewRequest + NewRecorder test handlers without binding port.
    Q5AdvancedTest concurrent code?
    go test -race; stress tests; channel synchronization assertions.

    Summary

    testing package + table-driven tests are Go standard. Mock via interfaces; httptest for HTTP handlers. Run go test -race -cover in CI on every PR. Subtests with t.Run organize cases cleanly. Next lesson: benchmark testing for performance.

    Ready to mark this lesson complete?Track your journey across the entire course.