๐งช Go Advanced Testing: Your Quality Shield
One Metaphor: Think of testing like being a detective checking if your robot helper works perfectly. You give it puzzles, watch how it behaves, and make sure it never makes mistakes!
๐ฏ What Youโll Learn
Imagine you built a LEGO castle. Before showing it to friends, youโd:
- Check if all pieces are connected
- Test if doors open properly
- Make sure it doesnโt fall apart
Thatโs exactly what advanced testing does for your Go code!
๐ Table-Driven Tests
The Story
Picture this: Youโre a teacher grading math homework. Instead of writing separate rules for each student, you make one checklist and run every studentโs paper through it!
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"two positives", 2, 3, 5},
{"with zero", 5, 0, 5},
{"negatives", -1, -2, -3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.expected {
t.Errorf("got %d, want %d",
got, tt.expected)
}
})
}
}
Why This Is Amazing
- One test function handles many scenarios
- Add new cases = just add a row to the table
- Each test has a name so you know which failed
graph TD A["Define Test Cases"] --> B["Loop Through Each"] B --> C["Run t.Run"] C --> D{Pass?} D -->|Yes| E["โ Next Case"] D -->|No| F["โ Report Error"] E --> B F --> B
๐ Testing HTTP Handlers
The Story
Your code has a door (HTTP handler) where visitors knock and ask for things. You need a pretend visitor to test if the door responds correctly!
Creating a Test Handler
func HelloHandler(w http.ResponseWriter,
r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
fmt.Fprintf(w, "Hello, %s!", name)
}
Testing It
func TestHelloHandler(t *testing.T) {
// Create fake request
req := httptest.NewRequest(
"GET",
"/?name=Alice",
nil,
)
// Create fake response recorder
rr := httptest.NewRecorder()
// Call the handler
HelloHandler(rr, req)
// Check the response
if rr.Code != http.StatusOK {
t.Errorf("wrong status: %d", rr.Code)
}
expected := "Hello, Alice!"
if rr.Body.String() != expected {
t.Errorf("got %s, want %s",
rr.Body.String(), expected)
}
}
The Magic Parts
| Component | What It Does |
|---|---|
httptest.NewRequest |
Creates a pretend visitor |
httptest.NewRecorder |
Catches what the door says back |
rr.Code |
The status number (200 = OK!) |
rr.Body |
The actual message returned |
๐ฆ The httptest Package
Your Testing Toolbox
The httptest package gives you superpowers to test web code without starting a real server!
Tool 1: NewRecorder
Records what your handler says:
rr := httptest.NewRecorder()
// Use rr as http.ResponseWriter
handler.ServeHTTP(rr, req)
// Now check rr.Code and rr.Body
Tool 2: NewRequest
Creates fake HTTP requests:
// GET request
req := httptest.NewRequest("GET", "/", nil)
// POST with body
body := strings.NewReader(`{"name":"Go"}`)
req := httptest.NewRequest(
"POST", "/api", body,
)
req.Header.Set(
"Content-Type",
"application/json",
)
Tool 3: NewServer
Creates a real temporary server for bigger tests:
server := httptest.NewServer(
http.HandlerFunc(HelloHandler),
)
defer server.Close()
// Now use server.URL to make real requests!
resp, _ := http.Get(server.URL + "?name=Go")
graph TD A["httptest Package"] --> B["NewRecorder"] A --> C["NewRequest"] A --> D["NewServer"] B --> E["Capture Response"] C --> F["Fake HTTP Request"] D --> G["Real Test Server"]
๐ Integration Testing
The Story
Unit tests check one LEGO piece. Integration tests check if many pieces work together like a real castle!
Example: Testing Database + Handler
func TestUserAPI(t *testing.T) {
// Setup: real database connection
db := setupTestDB(t)
defer db.Close()
// Create handler with real DB
handler := NewUserHandler(db)
server := httptest.NewServer(handler)
defer server.Close()
// Test creating a user
resp, err := http.Post(
server.URL+"/users",
"application/json",
strings.NewReader(
`{"name":"Alice"}`,
),
)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 201 {
t.Errorf("create failed: %d",
resp.StatusCode)
}
// Test fetching the user
resp, _ = http.Get(
server.URL + "/users/1",
)
// ... verify response
}
Key Differences
| Unit Test | Integration Test |
|---|---|
| Tests one function | Tests multiple systems |
| Uses mocks/fakes | Uses real dependencies |
| Super fast | Slower but realistic |
| Catches logic bugs | Catches connection bugs |
Best Practices
- Isolate tests - each test gets fresh data
- Clean up - remove test data after
- Use test tags - separate from unit tests
//go:build integration
// Run with: go test -tags=integration
๐ Test Fixtures
The Story
Fixtures are like recipe ingredients you prepare before cooking. You set up test data once and reuse it!
Loading Test Data
Create a testdata folder (Go ignores this in builds):
myproject/
โโโ handler.go
โโโ handler_test.go
โโโ testdata/
โโโ user.json
โโโ expected.json
Using Fixtures
func TestParseUser(t *testing.T) {
// Load fixture file
data, err := os.ReadFile(
"testdata/user.json",
)
if err != nil {
t.Fatal(err)
}
// Use in test
var user User
json.Unmarshal(data, &user)
if user.Name != "Test User" {
t.Error("wrong name")
}
}
Golden Files Pattern
Compare output against saved โgoldenโ files:
func TestRender(t *testing.T) {
got := RenderTemplate(data)
golden := "testdata/expected.html"
// Update golden files with -update flag
if *update {
os.WriteFile(golden, got, 0644)
return
}
want, _ := os.ReadFile(golden)
if !bytes.Equal(got, want) {
t.Error("output doesn't match")
}
}
graph TD A["Test Fixtures"] --> B["testdata/ folder"] B --> C["JSON files"] B --> D["Golden files"] B --> E["Sample inputs"] C --> F["Load & Parse"] D --> G["Compare Output"] E --> H["Feed to Functions"]
๐ฒ Fuzzing
The Story
Imagine a robot randomly pressing buttons on your calculator millions of times to find bugs youโd never think of!
What Fuzzing Does
- Generates random inputs automatically
- Finds edge cases humans miss
- Saves crashing inputs as proof
Writing a Fuzz Test
func FuzzReverse(f *testing.F) {
// Add seed corpus (starting examples)
f.Add("hello")
f.Add("ไธ็")
f.Add("")
// The fuzz target
f.Fuzz(func(t *testing.T, s string) {
rev := Reverse(s)
doubleRev := Reverse(rev)
// Property: reversing twice = original
if s != doubleRev {
t.Errorf("double reverse failed")
}
})
}
Running Fuzz Tests
# Run for 30 seconds
go test -fuzz=FuzzReverse -fuzztime=30s
# Found failures saved in testdata/fuzz/
Key Concepts
| Term | Meaning |
|---|---|
| Seed Corpus | Starting examples you provide |
| Generated Corpus | New inputs fuzzer creates |
| Crash | When input causes failure |
| Coverage | Code paths the fuzzer explored |
When Fuzzing Shines
- Parsing functions (JSON, XML, etc.)
- String manipulation
- Encoding/decoding
- Any function taking
[]byteorstring
graph TD A["Start Fuzzing"] --> B["Use Seed Corpus"] B --> C["Generate Random Input"] C --> D["Run Function"] D --> E{Crash?} E -->|Yes| F["Save Failing Input"] E -->|No| G["Mutate Input"] G --> C F --> H["Fix Bug!"]
๐ Putting It All Together
Your Testing Pyramid
graph TD A["Unit Tests - Many & Fast"] --> B["Table-Driven"] A --> C["httptest"] D["Integration Tests - Some"] --> E["Real DB + Server"] F["Fuzz Tests - Continuous"] --> G["Find Edge Cases"] B --> H["๐ Confident Code"] C --> H E --> H G --> H
Quick Reference
| Tool | Use When |
|---|---|
| Table-driven | Testing many inputs |
| httptest | Testing HTTP handlers |
| Integration | Testing system connections |
| Fixtures | Need consistent test data |
| Fuzzing | Finding unknown edge cases |
๐ You Did It!
Youโre now equipped with Goโs advanced testing arsenal:
- โ Table-driven tests - One function, many scenarios
- โ HTTP testing - Fake visitors for your handlers
- โ httptest package - Your testing toolbox
- โ Integration tests - Check the whole system
- โ Fixtures - Prepared test ingredients
- โ Fuzzing - Robot button-masher finds bugs
Go forth and write bulletproof code! ๐ก๏ธ
