Digging Deeper

Testing

Combine focused package tests with full application HTTP integration tests.

By Guillermo Alvarez - Published - Updated

Test at the ownership boundary

Keep focused tests beside the package they exercise:

  • Framework renderer, dispatch, application, and routing tests live in golazy.
  • Service tests live beside application services.
  • Adapter tests live beside lib packages.
  • Executable helper tests live in cmd/app.

Complete application behavior belongs in sample_app/test.

Construct the real handler

Integration tests should use the same composition path as main:

func application() http.Handler {
    return appinit.App()
}

This catches broken dependency wiring, route registration, template rendering, dispatch, and public-file setup together.

Exercise requests

Use the standard library:

request := httptest.NewRequest(http.MethodGet, "/posts", nil)
response := httptest.NewRecorder()

application().ServeHTTP(response, request)

if response.Code != http.StatusOK {
    t.Fatalf("status = %d", response.Code)
}

Useful assertions include:

  • Status code.
  • Content-Type.
  • ETag and If-None-Match behavior.
  • Cache-Control for logical and permanent asset URLs.
  • Allow for method errors.
  • Escaped or rendered body content.
  • 404 behavior for missing records and files.
  • Embedded public-file responses and asset permalinks.

The rendered page should link content-hashed asset URLs through asset_path. An integration test can extract the stylesheet URL and request it directly:

body := response.Body.String()
match := regexp.MustCompile(`href="([^"]*styles-[^"]+\.css)"`).FindStringSubmatch(body)
if match == nil {
    t.Fatal("stylesheet permalink not found")
}

asset := httptest.NewRecorder()
application().ServeHTTP(
    asset,
    httptest.NewRequest(http.MethodGet, match[1], nil),
)

if got := asset.Header().Get("Cache-Control"); !strings.Contains(got, "immutable") {
    t.Fatalf("Cache-Control = %q, want immutable", got)
}
if asset.Header().Get("ETag") == "" {
    t.Fatal("asset ETag is empty")
}

Also test a matching If-None-Match request returns 304 Not Modified. Logical asset paths should keep a revalidation-friendly cache policy, while permanent asset paths should be safe to cache indefinitely.

Verify request-local controllers

Controller state is mutable, so integration tests should exercise concurrent requests:

var wait sync.WaitGroup
for range 20 {
    wait.Add(1)
    go func() {
        defer wait.Done()
        response := httptest.NewRecorder()
        handler.ServeHTTP(
            response,
            httptest.NewRequest(http.MethodGet, "/posts", nil),
        )
        if response.Code != http.StatusOK {
            t.Errorf("status = %d", response.Code)
        }
    }()
}
wait.Wait()

Run this under the race detector.

Test rendering boundaries

Framework renderer tests should verify:

  • Ordinary values are HTML-escaped.
  • View output is composed into the layout.
  • Missing views and layouts return contextual errors.
  • A renderer cannot be initialized without the default layout.

Application tests should verify only the behavior the application owns.

Release verification

For the framework:

go test ./...
go test -race ./...
go vet ./...

For the sample application:

go test ./...
go test -race ./...
go vet ./...
go build -o /tmp/sample-app ./cmd/app

Use a temporary GOCACHE and CC=/usr/bin/gcc when required by the managed development environment.