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

Regenerate JavaScript library assets before Go verification when JavaScript manifest or package files changed:

lazy js

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.