Gopher says
Before any train leaves the station, it passes a safety inspection. In Go,
that’s go test. No framework to install, no config files - just naming
conventions and the testing package.
In baton-junction, every client method and builder function is testable in
isolation - the Validate method, the display name logic, URL construction -
all verifiable with a single go test command.
Test File Conventions
Test files live right next to the code they test - same directory, same package:
pkg/connector/
connector.go
connector_test.go
users.go
users_test.go Two rules make this work:
- Files ending in
_test.goare only compiled bygo test - Test functions start with
Testand take*testing.T
func TestMapUserStatus(t *testing.T) {
...
} Table-Driven Tests
When you have many variations of the same behavior (different inputs, different expected outputs), repeating test functions gets tedious fast. Go’s table-driven tests solve this: instead of one function per scenario, define a table:
func TestMapUserStatus(t *testing.T) {
tests := []struct {
input string
want v2.UserTrait_Status_Status
}{
{"active", v2.UserTrait_Status_STATUS_ENABLED},
{"disabled", v2.UserTrait_Status_STATUS_DISABLED},
{"suspended", v2.UserTrait_Status_STATUS_DISABLED},
{"deleted", v2.UserTrait_Status_STATUS_DELETED},
{"unknown", v2.UserTrait_Status_STATUS_UNSPECIFIED},
{"", v2.UserTrait_Status_STATUS_UNSPECIFIED},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := mapUserStatus(tt.input)
if got != tt.want {
t.Errorf("mapUserStatus(%q) = %v, want %v",
tt.input, got, tt.want)
}
})
}
}
Adding a test case is one line. Missing cases are visually obvious. t.Run creates named subtests so failures show TestMapUserStatus/active instead of just TestMapUserStatus.
t.Errorf vs t.Fatalf
When a test fails, you get to choose: collect all failures in one run, or stop at the first one. The choice matters for debugging. Here’s the distinction:
t.Errorf- marks failure, keeps running (collect all failures)t.Fatalf- marks failure, stops immediately (prevents cascading panics)
r, err := resource.NewUserResource(...)
if err != nil {
t.Fatalf("unexpected error: %v", err) // r is nil, can't continue
}
if r.DisplayName != tt.wantDisplay {
t.Errorf("DisplayName = %q, want %q", r.DisplayName, tt.wantDisplay)
}
Use Fatalf when setup fails. Use Errorf when checking individual fields.
Mock HTTP Servers
Your connector talks to external APIs, and you can’t hit production during tests. Mock HTTP servers let you simulate real API responses without leaving your machine - no network calls, no rate limits, no flakiness. The httptest package spins up real HTTP servers in your test process:
func TestGetGroups(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) {
writeJSON(t, w, ListResponse[Group]{
Data: []Group{{ID: "g1", Name: "Regional Pass"}},
})
})
c := newTestClient(t, mux)
groups, _, err := c.GetGroups(context.Background(), "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if groups[0].Name != "Regional Pass" {
t.Errorf("expected Regional Pass, got %s", groups[0].Name)
}
}
The test server catches bugs: JSON serialization errors, wrong URL paths, incorrect HTTP methods.
Functions that serve tests (not are tests) should call t.Helper(). This
makes failure output report the caller’s line number instead of the helper’s.
The baton-junction test suite uses helpers like newTestServer,
writeJSON, and newTestClient - each starts with t.Helper().
Running Tests
Run all tests in the project recursively with verbose output:
Run only the TestMapUserStatus/active subtest in the connector package:
Add at least 3 test cases to the tests table that cover the displayName fallback chain: full name, first name only, email fallback, and optionally the ID fallback.
Gopher says
Go testing is refreshingly simple: naming conventions replace configuration,
tables replace duplication, and httptest catches HTTP bugs.
In baton-junction, we test the display name fallback chain, URL construction, error classification, and full HTTP round-trips - all with zero test framework dependencies.
Next: reading real connector code!
Next Lesson
Reading Connector Code

