Overview
End-to-end tests exercise the deployed system through its public surface — HTTP API,
browser UI, or both — with real infrastructure underneath. Go needs less specialist
tooling here than the JVM: a plain net/http client plus encoding/json is a perfectly
good API-testing DSL, so there is no REST-Assured to learn. The genuinely third-party
parts are browser automation (playwright-go, rod, chromedp) and consumer-driven contracts
(pact-go), which trade breadth of E2E for precision and speed.
Key points
- API E2E is plain
net/http: build a tiny typed client helper per suite (do-request, decode, assert status) and the tests read as well as any DSL — see the example. - E2E tests are still
go test: same runner, same-runfiltering, same CI wiring; point the suite at a base URL from the environment. - Browser automation options:
playwright-go(bindings to the Playwright driver — auto-waiting, tracing, multi-browser),go-rod/rod(pure-Go DevTools protocol, no Node dependency),chromedp(the veteran DevTools library, Chrome-only). Playwright for feature breadth, rod for a Go-native dependency graph. - Contract testing with pact-go: consumer tests generate a pact file; the provider verifies it in its own CI. Catches integration breakage without a shared environment — prefer it to broad cross-service E2E.
- Environment management:
docker compose upfor service+dependencies suites (testcontainers-go can drive compose files too);kindfor applications whose contract includes Kubernetes manifests — spin a real cluster in CI,kubectl apply, test. - Keep the true-E2E layer thin: a smoke suite over deployed environments; everything that can move down to integration or contract level should.
- Flakiness is the tax: E2E suites die by nondeterminism, not by missing coverage — budget discipline for it from day one.
Details
Flakiness discipline
- Poll, never sleep: replace
time.Sleepwith eventually-style polling —require.Eventually(t, cond, 10*time.Second, 100*time.Millisecond)from testify, or a hand-rolled ticker loop. Asynchronous effects (queue consumption, cache warmup) become bounded waits instead of races. t.Parallelhazards: parallel E2E tests sharing one deployed environment collide on data (same user, same order IDs) and on rate limits. Either namespace all data per test (unique IDs, per-test tenants) or keep the E2E suite serial — a slow green suite beats a fast flaky one.- Own your test data: create what you assert on inside the test; never depend on seeded state that another test (or run) can mutate.
- Deadlines everywhere:
context.WithTimeouton every request so a wedged environment fails the test in seconds, not after the CI job timeout. - Quarantine, don't retry blindly: automatic retries hide real races; tag known-flaky tests, track them, fix or delete.
Choosing the E2E surface
| Surface | Tool | Use when |
|---|---|---|
| HTTP API | net/http + JSON helpers |
Always the first choice — fastest, most debuggable |
| Browser UI | playwright-go / rod / chromedp | The UI logic itself is the deliverable |
| Cross-service contract | pact-go | Two teams, two deploy cadences, one interface |
| Full stack in CI | docker compose / kind | Deployment topology is part of what you verify |
Examples
// A ten-line client helper replaces an API-testing DSL.
func postJSON[T any](t *testing.T, url string, body any) (int, T) {
t.Helper()
var out T
buf, _ := json.Marshal(body)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(buf))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("POST %s: %v", url, err)
}
defer resp.Body.Close()
_ = json.NewDecoder(resp.Body).Decode(&out)
return resp.StatusCode, out
}
func TestOrderLifecycle(t *testing.T) {
base := os.Getenv("E2E_BASE_URL")
status, order := postJSON[Order](t, base+"/orders", map[string]any{"sku": "A-1"})
if status != http.StatusCreated {
t.Fatalf("create: status %d", status)
}
require.Eventually(t, func() bool {
st, o := getJSON[Order](t, base+"/orders/"+order.ID)
return st == 200 && o.State == "confirmed"
}, 15*time.Second, 200*time.Millisecond, "order never confirmed")
}
Related
- Testing in Go — strategies & tooling map — parent map of the strategy portfolio.
- Integration testing — the boundary below: real dependencies without full deployment.
- REST in Go — the API style these suites drive from the outside.
- End-to-end testing in Java — REST Assured/Selenium land, for contrast.