5.6 KiB
description
| description |
|---|
| The ATDD loop this repo develops features with — write the Gherkin scenario first, watch it fail, write just enough code in cms/ to pass it, re-run, move to the next scenario. Use when adding or changing an endpoint, a validation rule, an error response, or any user-visible behaviour in cms/. |
The loop
One scenario at a time. Never write two scenarios' worth of code in one pass.
- Write the scenario in
tests/features/*.feature, in the language of the feature rather than of HTTP where you can. Name it for the behaviour (Refusing to register a video whose file was never uploaded), not the mechanism. - Run it and watch it fail.
cd tests && go test -count=1 -v ./...Confirm it fails for the reason you expect. A scenario that passes before you have written any code is testing nothing — go back to step 1. - Write just enough code in
cms/to turn that one scenario green. Not the next scenario's code. Not a generalisation you have no test for. - Re-run the suite — the whole file, not just the new scenario. Green means done; a scenario that used to pass and now does not means the new code changed published behaviour, which is a decision, not an accident.
- Next scenario. Repeat until the feature is described.
Only then tidy: extract helpers, rename, split files. The suite is what makes that safe, so do it with the tests green and re-run after.
Where things go
Tests are black box. tests/ is its own Go module (thamanyah/tests) that
imports nothing from cms/ and speaks only HTTP. Do not add _test.go files
inside cms/ — if a behaviour cannot be reached through the API, that is a
finding about the design, not a reason to reach past it.
| file | what goes in it |
|---|---|
tests/features/*.feature |
the scenarios |
tests/suite_test.go |
step registration, the per-scenario world |
tests/steps_test.go |
what each step does |
tests/client_test.go |
HTTP plumbing and wire types |
Production code keeps to the seams in CLAUDE.md: wire structs in
internal/api, domain types in internal/models, HTTP in internal/handlers,
SQL in internal/db/repositories, AWS in internal/services. "Just enough
code" still lands on the right seam — a handler that runs its own SQL is not
minimal, it is misplaced.
Running it
docker compose up -d # localstack + infra + cms (needs LOCALSTACK_AUTH_TOKEN)
docker exec cms ./cms migrate # see Migrations below
cd tests && go test -count=1 -v ./...
-count=1 defeats Go's test caching, which otherwise hides a re-run against a
changed service. -v prints the Gherkin: gobdd nests a subtest per feature,
scenario and step, so the step text appears in the output.
CMS_BASE_URL (default http://localhost:8081) points the suite elsewhere. If
nothing is serving, the suite skips rather than fails.
Migrations. Compose starts cms in server mode only — the migrate
container exists in the ECS task definition, not in docker-compose.yml. A new
migration is not applied by docker compose watch rebuilding the image.
Run docker exec cms ./cms migrate after adding one, or every scenario fails
on relation "…" does not exist.
The spec. Changing a handler, its annotations, or an internal/api struct
means regenerating the OpenAPI spec, or the build drifts from the published
contract:
$(go env GOPATH)/bin/swag init --generalInfo main.go --dir ./ --parseInternal --output ./docs
Reading a red run
gobdd does not stop a scenario at the first failing step. A step that fails leaves the later steps running against stale state, so one real failure produces a cascade of noisy ones. Read the first failure in a scenario and ignore the rest until it is fixed.
cannot find step definition for step: … is a legitimate red: the scenario is
written and the step is not. Add the function to steps_test.go, register it
in TestVideoUpload, and run again.
Writing steps
- Anchor every pattern with
^…$. gobdd matches unanchored and the first registered pattern that matches wins, so an unanchored pattern can silently shadow another step. - State moves between steps through the
*worldin the gobdd context, never package variables. It is held by pointer because gobdd clones the context between theBackgroundand the scenario steps. - A
Scenario Outlineruns every example row in one world. Steps must overwrite state, never accumulate it. - Reuse a step before writing a new one. Two steps that differ only in wording are two ways for the feature file to drift.
Asserting errors
Errors are RFC 9457 Problem Details on application/problem+json. title is
held identical across every occurrence of a given problem so clients can branch
on it — so assert the exact title, not just the status. A new error
condition means a new stable title, and a new @Failure annotation on the
handler.
When a status alone would be ambiguous, distinguish the cases in code rather
than in the assertion: a client's mistake and a server fault reaching the same
writeProblem call is the bug, and a sentinel error in
internal/handlers/errors.go plus errors.Is at the call site is how this repo
separates them.
@known-gap
Tag a scenario @known-gap when it pins what the service does today and
that differs from what it is documented to do. It still passes — it is a record,
not a failure — and it carries a comment saying what the contract promises and
what changing it would take. Fixing the code means changing the scenario in the
same commit and dropping the tag.
Do not use the tag to park a scenario you could not make pass.