Unit Testing
Go's testing package is built-in — no JUnit equivalent needed.
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.
flowchart LRTest[go test] --> Unit[Unit Tests]Unit --> Bench[Benchmarks]Bench --> Report[Coverage Report]
Step-by-step explanation
- go test ./... runs all tests recursively.
- go test -v verbose; -run TestName filter; -count=1 disable cache.
- Subtests t.Run(name, func(t){}) for isolated cases.
- httptest.NewRequest builds *http.Request for handler tests.
- testify/assert optional — stdlib sufficient for interviews.
- CI runs go test -race -coverprofile=coverage.out.
Practical code example
Table-driven service test and HTTP handler test with httptest:
package mainimport ("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 stringa, b intwant 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 withTestand 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?
Q2BeginnerTable-driven test benefit?
Q3IntermediateMock vs integration test?
Q4Intermediatehttptest usage?
Q5AdvancedTest concurrent code?
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.