Files
skills/skills/atdd/SKILL.md
T
mathias c6787e04d7
CI / Check / Test (push) Failing after 1s
CI / Tag release (push) Skipped
feat(index): generate the skills table from frontmatter
Second half of #29. The index was hand-maintained and had already drifted
once: dream and experiment-spec shipped with no row, their rows lost to a
squash-merge after a directory reorg. bin/check-index caught that after the
fact; generating removes the class.

The curated summary and trigger move into each SKILL.md as `summary:` and
`use_when:`, carried over verbatim from the existing table rather than derived
from `description:` — description is a paragraph written for model selection,
and truncating it would have produced a worse table than the one it replaced.
Knowledge now lives once, next to the skill it describes, which is the argument
that made a hand-maintained index the wrong shape to begin with.

The table is written between markers, so the wiring docs, the relationships
tree and the trigger table below it are untouched — those are prose, not data.
Rows are alphabetical now; the conceptual grouping the old order carried lives
in the relationships tree, which is the better place for it. A fourth column
shows each skill's surfaces, which is what makes #29's field visible rather
than merely enforced.

bin/check-index and its test are deleted: a generated table cannot drift in
either direction, so `generate-index --check` subsumes both of its assertions.
Verified by mutation — deleting the tdd row turns `task check` red with
"SKILLS_INDEX.md is out of date".

Closes #29
Bump-Type: minor
2026-08-29 11:23:14 +02:00

8.5 KiB

name, surfaces, summary, use_when, description
name surfaces summary use_when description
atdd
claude-code
library
Acceptance Test-Driven Development workflow Feature development with acceptance criteria Implement user stories using the ATDD workflow — Red (failing acceptance test) → Green (minimal implementation) → Refactor. One story at a time.

Acceptance Test-Driven Development (ATDD)

Overview

ATDD extends TDD to the acceptance level. You write tests that capture the behavior described in a user story's acceptance criteria — then implement just enough to make them pass.

Input required: User stories with clear acceptance criteria. Load user-stories skill first.

Core Methodology: Red-Green-Refactor at Acceptance Level

RED    → Write failing acceptance test (one story at a time)
GREEN  → Write minimal code to make it pass
REFACTOR → Clean up code (load clean-code skill)
COMMIT → Commit with reference to the story

Key principle: Never mix phases. RED is only writing tests. GREEN is only making tests pass. REFACTOR is only cleaning up.

Stop and confirm before advancing from RED to GREEN, and from GREEN to REFACTOR.

Working on One Story at a Time

Pick the highest-priority story from the backlog. Do not start a second story until the first is done (committed, clean, all tests green).

Phase 1: RED — Write Failing Acceptance Test

Translate the story's acceptance criteria into executable tests.

For each acceptance criterion:

Given [context]
When [action]
Then [observable outcome]

Becomes a test case.

Go ATDD Example

Story: "Users can register with email and password"

Acceptance criteria:

  • Given valid email and password, registration succeeds and returns the new user
  • Given an email already in use, registration returns ErrDuplicateEmail
  • Given an invalid email format, registration returns ErrInvalidEmail
  • Given a password shorter than 8 chars, registration returns ErrWeakPassword

Acceptance tests (write these BEFORE implementation):

func TestUserRegistration_Acceptance(t *testing.T) {
    tests := []struct {
        name    string
        email   string
        pass    string
        wantErr error
    }{
        {
            name:    "valid registration creates user",
            email:   "alice@example.com",
            pass:    "securePass1!",
            wantErr: nil,
        },
        {
            name:    "duplicate email returns ErrDuplicateEmail",
            email:   "existing@example.com",
            pass:    "securePass1!",
            wantErr: ErrDuplicateEmail,
        },
        {
            name:    "invalid email format returns ErrInvalidEmail",
            email:   "not-an-email",
            pass:    "securePass1!",
            wantErr: ErrInvalidEmail,
        },
        {
            name:    "short password returns ErrWeakPassword",
            email:   "alice@example.com",
            pass:    "short",
            wantErr: ErrWeakPassword,
        },
    }

    store := NewInMemoryUserStore()
    // Pre-seed duplicate email
    store.Seed(User{Email: "existing@example.com"})

    svc := NewUserService(store)

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            _, err := svc.Register(context.Background(), tt.email, tt.pass)
            if tt.wantErr != nil {
                assert.ErrorIs(t, err, tt.wantErr)
            } else {
                assert.NoError(t, err)
            }
        })
    }
}

Run and verify they fail:

go test -run TestUserRegistration_Acceptance ./...
# FAIL: UserService.Register undefined

The test must fail because the implementation doesn't exist yet. If it passes, you're testing existing behavior. Check your test.

STOP here. Confirm before moving to GREEN.

Phase 2: GREEN — Minimal Implementation

Implement the absolute minimum code necessary to make the acceptance tests pass. Ugly code is acceptable at this stage. The goal is green, not clean.

// Minimal implementation — prioritize passing tests over elegance
func (s *UserService) Register(ctx context.Context, email, password string) (User, error) {
    if !strings.Contains(email, "@") {
        return User{}, ErrInvalidEmail
    }
    if len(password) < 8 {
        return User{}, ErrWeakPassword
    }
    if s.store.Exists(ctx, email) {
        return User{}, ErrDuplicateEmail
    }
    u := User{ID: newID(), Email: email}
    if err := s.store.Save(ctx, u); err != nil {
        return User{}, fmt.Errorf("save user: %w", err)
    }
    return u, nil
}

Verify GREEN:

go test -run TestUserRegistration_Acceptance ./...
# ok
go test ./... # All other tests still pass
go test -race ./...

STOP here. Confirm before moving to REFACTOR.

Phase 3: REFACTOR — Clean Up

With tests green, clean the code. Do not change behavior.

During REFACTOR, load and follow the clean-code skill.

Apply:

  • Extract functions for complex logic
  • Better naming
  • Remove duplication
  • Apply SOLID principles
  • For design decisions, load solid skill

After every change: go test ./... must stay green.

Do NOT refactor the acceptance tests themselves unless they have a bug. The tests are your specification — changing them changes what you've committed to.

STOP here. Confirm before committing.

Phase 4: COMMIT

Commit the completed story. Reference the story in the commit message.

git commit -m "feat: user registration with email validation

Implements acceptance criteria for US-001 (User Registration).
- Valid email/password creates a user
- Duplicate email returns ErrDuplicateEmail
- Invalid email format returns ErrInvalidEmail
- Short password returns ErrWeakPassword"

For Test Design Questions

When writing acceptance tests, load the test-design skill to ensure:

  • Tests are understandable (Farley: Understandable)
  • Tests don't couple to implementation details (Farley: Maintainable)
  • Tests are repeatable across environments (Farley: Repeatable)

For Code Review Before Commit

Load the code-review skill before committing to check:

  • Error handling follows fmt.Errorf("op: %w", err) pattern
  • Context propagation is correct
  • No race conditions in concurrent code
  • Exported API is minimal

For Refactoring Guidance

Load the refactoring skill for systematic techniques to apply during REFACTOR phase.

For Clean Code Principles

Load the clean-code skill during REFACTOR phase to apply:

  • Naming conventions
  • Function size and responsibility
  • SOLID principles

Go-Specific ATDD Notes

Acceptance Test Location

Place acceptance tests in _test.go files with the _acceptance_test.go suffix or //go:build acceptance build tag to separate from fast unit tests:

//go:build acceptance

package service_test
go test ./...              # Unit tests only (fast)
go test -tags=acceptance ./...  # Include acceptance tests

In-Memory Implementations for Acceptance Tests

Use in-memory implementations of repositories for acceptance tests:

type InMemoryUserStore struct {
    mu    sync.Mutex
    users map[string]User
}

func (s *InMemoryUserStore) Save(ctx context.Context, u User) error {
    s.mu.Lock()
    defer s.mu.Unlock()
    if _, exists := s.users[u.Email]; exists {
        return ErrDuplicateEmail
    }
    s.users[u.Email] = u
    return nil
}

func (s *InMemoryUserStore) Exists(ctx context.Context, email string) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    _, exists := s.users[email]
    return exists
}

This keeps acceptance tests fast and deterministic.

Given-When-Then in Go

The t.Run structure maps naturally to Given-When-Then:

t.Run("given valid credentials, when logging in, then returns token", func(t *testing.T) {
    // Given: valid user exists
    store := InMemoryUserStore{...}

    // When: login is called
    token, err := svc.Login(ctx, email, pass)

    // Then: token is returned
    require.NoError(t, err)
    assert.NotEmpty(t, token)
})

Verification Checklist

Before committing:

  • All acceptance tests pass
  • All existing tests still pass
  • Race detector clean: go test -race ./...
  • REFACTOR phase complete — code is clean
  • Commit message references the user story
  • No behavior added beyond the acceptance criteria

Cross-References

  • Requires: user-stories skill output (stories with acceptance criteria)
  • During REFACTOR: load clean-code skill
  • For test quality: load test-design skill
  • For code review: load code-review skill
  • For refactoring guidance: load refactoring skill
  • Foundation: load tdd skill for core TDD methodology