SuperAPI AI Directory

golang-unit-testing

Testing & quality

View repository

Updated 10-15 20:32

About this skill

Golang Unit Testing

Test risky behavior, not private details. Prefer clear table-driven tests over fragile suites.

Stack and scope

Ask Role
testing Test*, t.Run, t.Helper
httptest In-memory HTTP requests
Gin gin.New() / test mode / real handlers
fakes Ports at the edge (repo, clock, bus)

Use testify only if the project already depends on it. Concurrency: run with -race on the touched packages.

What to prioritize

  • Authz and ownership
  • Validation and HTTP status
  • Mutations and domain errors (404, 409)
  • Regressions that already occurred (bug → test)
  • Pure helpers and policies

Table-driven

func TestParseCursor(t *testing.T) {
    tests := []struct {
        name    string
        in      string
        want    Cursor
        wantErr bool
    }{
        {name: "vazio", in: "", want: Cursor{}},
        {name: "inválido", in: "%%%", wantErr: true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := ParseCursor(tt.in)
            if tt.wantErr {
                if err == nil {
                    t.Fatalf("expected error")
                }
                return
            }
            if err != nil {
                t.Fatalf("unexpected err: %v", err)
            }
            if got != tt.want {
                t.Fatalf("got %#v want %#v", got, tt.want)
            }
        })
    }
}
  • Name: behavior + condition.
  • Independent cases; no implicit global order.
  • t.Helper() in shared asserts.

Gin handlers

func TestCreateTodo_Unauthorized(t *testing.T) {
    gin.SetMode(gin.TestMode)
    r := gin.New()
    r.POST("/todos", createTodo) // injete deps fake

    req := httptest.NewRequest(http.MethodPost, "/todos", bytes.NewBufferString(`{"title":"x"}`))
    req.Header.Set("Content-Type", "application/json")
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    if w.Code != http.StatusUnauthorized {
        t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
    }
}
  • Set up the minimal router needed (real auth middleware + fake deps).
  • Prefer ServeHTTP on the engine rather than testing only with CreateTestContext when the middleware matters.
  • Compare status + relevant JSON fields, not the fragile entire body.

Fakes and edges

  • Interface at the edge (repository, mailer); in-memory fake in the test.
  • Do not mock the system under test.
  • Clock/UUID/rand: inject for determinism.
  • Real DB: reserve for integration tests; isolate with schema/transaction per test.

Concurrency and flaky

  • go test -race in packages with goroutines.
  • Avoid time.Sleep; use channels, conditions, or injected clocks.
  • Timeouts via context in tests that wait for events.
  • Clean global state (gin mode ok; avoid mutable package vars).

Organization

internal/service/service_test.go     # unit
internal/http/handlers/handler_test.go
  • _test external package (package foo_test) when testing the public API.
  • Internal package when internal whites are needed, with care.
  • Small fixtures; builders instead of copied gigantic JSON.

Anti-patterns

  • A single monolithic test without t.Run
  • Asserting on an unstable error message as a contract
  • Dependence on real network/clock without control
  • Testing only the happy path of authz
  • Excessive generated mocks for internal types
  • Ignoring -race in new concurrent code

Completion criteria

  • Risks of the change covered (authz, validation, errors)
  • Readable and deterministic table-driven tests
  • Handlers exercised via HTTP when the contract matters
  • Fakes only at the edge
  • go test (and -race if concurrent) passing in the touched packages