E2E Tests — Tasks¶
Each task ships a small Go service or CLI and asks you to write an end-to-end test against it. Treat the SUT as a black box. Build tag e2e is assumed for all tests in test/e2e/.
Task 1 — API smoke test¶
Service cmd/orders exposes POST /orders (creates an order) and GET /orders/{id} (returns one). Both require header Authorization: Bearer $TOKEN.
Write TestE2E_OrderRoundTrip that:
- Reads
E2E_BASE_URLandE2E_TOKENfrom env. Skip the test if either is empty. - Creates an order with body
{"sku": "WIDGET", "qty": 3}. - Polls
GET /orders/{id}until status isconfirmed, with a 10s deadline and 200ms interval. - Asserts the response body has the original sku and qty.
Use net/http directly. No third-party HTTP libraries.
Acceptance: the test passes against a healthy service, and fails clearly (naming the exceeded deadline) when the worker that confirms orders is stopped.
Task 2 — Tenant isolation helper¶
Write a helper func newTenant(t *testing.T) Tenant that:
- Creates a tenant via
POST /admin/tenantswith namee2e-<random>. - Returns a
Tenant{ID, AdminToken}struct. - Registers
t.Cleanupto delete the tenant.
Then write two parallel tests that each call newTenant and confirm they receive distinct IDs. The point is to prove that t.Parallel() works with the helper.
Acceptance: go test -tags=e2e -race -parallel 4 ./test/e2e/... passes and the admin API logs show two distinct CreateTenant calls.
Task 3 — Eventually helper¶
Without using testify, write a generic poll helper:
It must:
- Call
fnimmediately, then everytickuntilfnreturns true ordeadlineelapses. - On timeout, call
t.Fatalfwith the deadline and tick used. - Respect
t.Context()if available (Go 1.24+).
Cover it with a unit test that exercises both success and timeout paths.
Bonus: add a func Never(t, deadline, tick, fn) variant that fails if fn ever becomes true within the deadline — useful for asserting "this stays false for N seconds."
Task 4 — chromedp login¶
The webapp at $E2E_BASE_URL has /login (email + password form) and /dashboard (shows #greeting after login). Using github.com/chromedp/chromedp:
- Start a headless browser in
TestMain, reuse it across tests viachromedp.NewContext. - Write
TestE2E_LoginShowsGreetingthat logs in with seed useralice@example.com/secretand asserts the greeting text matches"Hello, Alice". - On failure, capture a screenshot to
$E2E_ARTIFACTS_DIR.
Acceptance: the screenshot includes the unexpected greeting text and the suite exits non-zero.
Task 5 — playwright-go variant¶
Repeat task 4 using github.com/playwright-community/playwright-go. The suite must skip cleanly if PWDEBUG=1 is set and the browser binary is not installed.
Compare the wall-clock time and the artefact quality of chromedp vs playwright-go. Note the differences in the README of the suite.
Task 6 — CLI E2E¶
The binary bin/mytool accepts mytool greet --name=Alice and prints Hello, Alice to stdout. Build the binary in TestMain via go build -o bin/mytool ./cmd/mytool and write TestE2E_GreetPrintsName using os/exec.
Bonus: write TestE2E_GreetInteractive that uses github.com/creack/pty to drive an interactive prompt (mytool greet with no flags asks for the name on stdin).
Acceptance: both tests pass on Linux and macOS. (The PTY test can be skipped on Windows.)
Task 7 — Ephemeral docker-compose env¶
Write a TestMain that:
- Reads
E2E_BASE_URL. If set, use it (assume external env). - Otherwise, shells out to
docker compose up -dfromtestdata/compose/, waits for/healthon the published port, and tears down on exit. - Sets
E2E_BASE_URLfor the suite.
Tests must not know which mode they are in.
Bonus: handle Ctrl-C in TestMain so a developer-killed run still tears down the compose stack.
Task 8 — Failure artefacts¶
Extend the suite from any prior task with a defer in each test that, on t.Failed():
- Writes the last 200 lines of each compose service log to
$E2E_ARTIFACTS_DIR/<test>/<svc>.log. - For browser tests, also writes a PNG screenshot and a DOM dump.
Demonstrate the path by introducing a deliberate failing assertion.
Acceptance: the artefacts directory contains a per-test subdirectory whose contents are sufficient to diagnose the failure without re-running the suite.
Task 9 — Retry with backoff¶
Wrap an HTTP call helper so that retryable failures (connection reset, HTTP 502/503/504) are retried up to 5 times with exponential backoff (100ms × 2^n) and jitter. Non-retryable failures (4xx other than 408 and 429) return immediately. Cover with a unit test that uses httptest.NewServer to inject specific responses.
Acceptance: the unit test demonstrates both retry and immediate-return behaviour. The helper does not retry on context cancellation.
Task 10 — Per-tenant parallel suite¶
Convert a 5-test sequential suite into a parallel suite using the newTenant helper from Task 2. Verify that:
- All tests pass with
go test -tags=e2e -parallel 4 ./test/e2e/.... - The wall-clock time drops to roughly
max(individual durations)and notsum(individual durations). - No test leaks data into another tenant (assert by counting rows in the shared DB after each test).
Bonus: record the before/after timings in a BENCHMARKS.md next to the suite.
Task 11 — kind cluster E2E¶
Stand up a kind cluster in TestMain, apply the manifests in testdata/k8s/, port-forward the service, and run the API smoke test from Task 1 against it. Acceptance: the cluster is gone after the test binary exits and kind get clusters shows no leftovers.
Task 12 — Contract test bridge¶
Generate a Go HTTP client from the service's OpenAPI spec (any generator of your choice). Rewrite the Task 1 test to use the generated client. Observe what happens when the spec drifts from the server — change a field name in the spec and re-run the test.
Task 13 — Quarantine harness¶
Add a quarantined(name, ticket) helper that reads a YAML file of quarantined test names and skips matching tests with a useful message pointing at the ticket. Demonstrate by quarantining a test, observing it skip in CI, removing it from the YAML, and observing it run again.
# quarantine.yml
- name: TestE2E_OldFlakyThing
ticket: https://bugs.example.com/12345
added: 2026-05-15
Acceptance: the suite reports quarantined tests in its output but does not fail on them.
Task 14 — Request ID propagation¶
Every HTTP call from your E2E client sends an X-Request-ID header unique per test (use uuid.NewString). The SUT's logs include this ID in every log line for the request. Demonstrate the trace by:
- Running a test.
- Capturing the request ID from the test log.
- Searching the SUT's log output for that ID.
- Confirming you can reconstruct the request lifecycle from the log lines alone.
Task 15 — Smoke vs full¶
Tag five tests with TestE2E_Smoke_ prefix; leave the rest of the suite tagged normally. Run:
# smoke only
go test -tags=e2e -run 'TestE2E_Smoke_' ./test/e2e/...
# full
go test -tags=e2e ./test/e2e/...
Confirm both subsets pass. Measure the wall-clock time of each. Document the smoke set's selection rationale in a SMOKE.md file.
Task 16a — Per-test request ID dashboard¶
Extend the artefact upload with a JSON manifest:
{
"test": "TestE2E_OrderRoundTrip",
"request_ids": ["abc-...", "def-..."],
"duration_ms": 1234,
"outcome": "fail"
}
A small dashboard reads these manifests and lets a developer click through to the SUT's log query for any request ID. Acceptance: a failed test produces a clickable manifest within the artefact directory.
Task 16 — Artefact viewer¶
Build a tiny static-site generator that reads $E2E_ARTIFACTS_DIR and produces an HTML page per test with links to its screenshot, logs, and HTTP transcript. Useful for sharing E2E failures in a pull-request comment or a chat message.
Acceptance: a single command transforms an artefact directory into a browseable HTML report. The report works offline.
Task 17 — Eventually with backoff¶
Extend the Eventually helper from Task 3 to support exponential backoff with a configurable initial tick and max tick. Demonstrate with a probe that becomes true at a random time within a 30-second window; show that the helper polls aggressively at first and backs off, keeping wall-clock idle low.
Acceptance: the test passes with both fixed-tick and backoff modes; the backoff mode performs fewer total polls for long waits.
Task 18 — Cross-tenant negative test¶
Using the newTenant helper, write a test that:
- Creates two tenants A and B.
- Creates an order in A.
- Attempts to read the order via B's credentials.
- Asserts the response is 404 (not 403 — 403 leaks existence).
Acceptance: the test passes against a correctly-implemented service and fails clearly against one that returns 403.
Task 19 — Kafka event observer¶
For a SUT that publishes Kafka events, write a small in-process consumer that records the events the SUT emits during a test. The test asserts on the recorded events. Use a unique consumer group per test to avoid cross-test interference.
Acceptance: tests assert "the SUT emitted event X within 10 seconds of action Y" without polling the database.
Task 20 — Nightly vs PR runner config¶
Write two CI configurations (GitHub Actions YAML or equivalent): one for per-PR (smoke only, < 8 minutes) and one for nightly (full suite, < 30 minutes). Both upload artefacts on failure. The nightly config sends a Slack notification on failure; the PR config does not.
Task 21 — Two-run idempotence check¶
Add a CI step that runs the full E2E suite twice in a row, in the same environment, without environment teardown between runs. The suite must pass both times. Acceptance: a deliberately broken cleanup (a test that does not delete its tenant) is detected on the second run by a row-count assertion in TestMain finalisation.
Task 22 — chromedp screenshot on every test¶
Modify the browser harness so every test (passing or failing) captures a screenshot at the end. Pass-time screenshots go to a "passes" subdirectory; fail-time go to "fails". Useful for visual regression review. Disable in CI by default; enable with E2E_CAPTURE_PASSES=1.
Task 23 — Per-test compose project¶
When the suite brings up its own compose stack, give each test binary a unique compose project name (e.g. e2e-${PID}) so two parallel go test invocations on the same host do not collide.
Acceptance: starting two suites simultaneously on one machine produces two isolated stacks; each tears down cleanly.
Task 24 — pty-driven CLI session¶
For the CLI binary, write an interactive session test that:
- Starts the binary via
github.com/creack/pty. - Sends three commands in sequence (
set name=Alice,greet,quit). - Reads the output between each command (use a deadline so a silent hang fails fast).
- Asserts on the final output.
Bonus: verify the binary handles a SIGINT (Ctrl-C) gracefully — the test sends a SIGINT and asserts the binary exits with code 130.
Task 25 — Webhook receiver¶
The SUT can be configured with a webhook URL. Write a test that:
- Stands up an in-process
httptest.NewServerto receive webhooks (this is allowed in E2E because the receiver is part of the test, not the SUT). - Configures the SUT to post webhooks to that URL.
- Triggers an action on the SUT.
- Asserts the webhook payload arrived within 30 seconds with the expected shape.
Acceptance: the test fails clearly when the webhook is misrouted (wrong URL) and when the payload shape is unexpected.