Some checks failed
release / tag (push) Has been cancelled
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]
295 lines
8.4 KiB
Markdown
295 lines
8.4 KiB
Markdown
---
|
|
name: atdd
|
|
description: 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):**
|
|
```go
|
|
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:**
|
|
```bash
|
|
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.
|
|
|
|
```go
|
|
// 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:**
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
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
|
|
//go:build acceptance
|
|
|
|
package service_test
|
|
```
|
|
|
|
```bash
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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
|