Files
skills/atdd/SKILL.md
Mathias d6a71e370e
Some checks failed
release / tag (push) Has been cancelled
chore: bootstrap skills library — 19 skills + installer + CI auto-tag
Phase 1 of mathias/skills extraction (infra#62 Track D — homelab
next-step plan addendum). Imports ~/dev/.skills/ verbatim (19 skill
dirs + SKILLS_INDEX.md) and adds the installation surface:

- Taskfile.yml — install / update / list / release / check targets
- install.sh — bootstrap installer for hosts without Task. Idempotent
  symlink wirer; default checkout at ~/.local/share/skills/ on every
  host; SKILLS_REF env var pins a tag (default: main).
- .gitea/workflows/release.yml — auto-tag every push to main by
  Bump-Type footer (major/minor/patch, default patch). Skipped when
  commit contains [skip-release].
- README — usage, versioning, contribution flow, secret-hygiene rule.

Phase 1 wires Claude Code only (~/.claude/skills/<name> global +
<repo>/.claude/skills/<name> per-repo). Phase 2 adds Crush, opencode,
antigravity, and gitea-resident agents (cobalt-dingo, agentsquad)
once their skill conventions are researched.

Public repo, markdown-only — no secrets, no client names. Verified
via pre-push grep before initial push.

[skip-release]
2026-05-24 14:59:54 +02:00

8.4 KiB

name, description
name description
atdd 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