122 lines
5.6 KiB
Markdown
122 lines
5.6 KiB
Markdown
---
|
|
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.
|
|
|
|
1. **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.
|
|
2. **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.
|
|
3. **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.
|
|
4. **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.
|
|
5. **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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
$(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 `*world` in the gobdd context, never
|
|
package variables. It is held by **pointer** because gobdd clones the context
|
|
between the `Background` and the scenario steps.
|
|
- A `Scenario Outline` runs 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.
|