opencode-workflow/skills/tdd/SKILL.md

297 lines
8.3 KiB
Markdown
Raw Permalink Normal View History

2026-04-08 23:53:15 +00:00
---
name: tdd
description: "Backend Agent uses this skill for Test-Driven Development. Follows Red-Green-Refactor cycle and vertical slicing principles, ensuring tests cover behavior rather than implementation details. Trigger: Implementation phase (Stage 9), integrated with go-backend-dev skill."
---
# /tdd — Test-Driven Development
Backend Agent uses this skill for Test-Driven Development.
## Core Philosophy
**Test behavior, not implementation details.**
Good tests verify behavior through public interfaces, describing the system "what" it does, not "how" it does it. Tests still pass after refactoring.
Bad tests are coupled to implementation: mocking internal collaborators, testing private methods. Tests fail after refactoring, but behavior hasn't changed.
## Anti-Pattern: Horizontal Slicing
**Don't write all tests first, then all implementations.** This is "horizontal slicing":
```
❌ Wrong way:
RED: test1, test2, test3, test4, test5
GREEN: impl1, impl2, impl3, impl4, impl5
✅ Correct way (vertical slicing):
RED→GREEN: test1 → impl1
RED→GREEN: test2 → impl2
RED→GREEN: test3 → impl3
```
Horizontal slicing produces low-quality tests:
- Tests written early verify "imagined" behavior, not "actual" behavior
- Tests become validators of data structures and function signatures, not user-observable behavior
- Tests are insensitive to real changes — they pass when behavior is broken, fail when behavior hasn't changed but after refactoring
## Flow
```
Confirm interface changes and test scope
Write first test (tracer bullet)
RED: Test fails
GREEN: Write minimal code to make test pass
Write next test
RED → GREEN loop
All behavior tests complete
REFACTOR: Refactor
Confirm all tests still pass
```
### Step Details
**1. Planning**
Before writing any code:
- [ ] Confirm which interface changes are needed with user
- [ ] Confirm which behaviors need testing (prioritize)
- [ ] Identify opportunities for deep modules (small interface, deep implementation)
- [ ] Design interfaces for testability
- [ ] List behaviors to test (not implementation steps)
- [ ] Get user approval for test plan
Ask: "What should the public interface look like? Which behaviors are most important to test?"
**You cannot test everything.** Confirm with user which behaviors are most important, focus testing effort on critical paths and complex logic, not every possible edge case.
**2. Tracer Bullet**
Write a test that confirms one thing about the system:
```
RED: Write first behavior test → Test fails
GREEN: Write minimal code to make test pass → Test passes
```
This is your tracer bullet — proving the end-to-end path works.
**3. Incremental Loop**
For each remaining behavior:
```
RED: Write next test → Fails
GREEN: Minimal code to make test pass → Passes
```
Rules:
- One test at a time
- Write only enough code to make current test pass
- Don't predict future tests
- Tests focus on observable behavior
**4. Refactoring**
After all tests pass, look for refactoring candidates:
- [ ] Extract duplicate logic
- [ ] Deepen modules (move complexity behind simple interfaces)
- [ ] Apply SOLID principles naturally
- [ ] Consider what new code reveals about existing code problems
- [ ] Run tests after each refactoring step
**Never refactor while in RED state. Get back to GREEN first.**
## Good Tests vs Bad Tests
### Good Tests
**Integration style**: Test through real interfaces, not mocking internal parts.
```go
// GOOD: Test observable behavior
func TestUserUsecase_CreateUser_Success(t *testing.T) {
mockRepo := new(mock.UserRepository)
uc := NewUserUsecase(mockRepo, logger)
mockRepo.On("GetByEmail", mock.Anything, "test@example.com").Return(nil, nil)
mockRepo.On("Create", mock.Anything, mock.AnythingOfType("*domain.User")).Return(nil)
user, err := uc.CreateUser(context.Background(), input)
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, "test@example.com", user.Email)
}
```
Characteristics:
- Tests behavior that users/callers care about
- Uses only public APIs
- Tests still pass after internal implementation refactoring
- Describes "what" instead of "how"
- One logical assertion per test
### Bad Tests
**Implementation detail testing**: Coupled to internal structure.
```go
// BAD: Test implementation details
func TestUserUsecase_CreateUser_CallsRepoCreate(t *testing.T) {
mockRepo := new(mock.UserRepository)
uc := NewUserUsecase(mockRepo, logger)
uc.CreateUser(context.Background(), input)
// This tests "how" instead of "what"
mockRepo.AssertCalled(t, "Create", mock.Anything, mock.Anything)
}
```
Red flags:
- Mocking internal collaborators just to verify they were called
- Testing private methods
- Asserting call counts or order
- Tests fail after refactoring but behavior unchanged
- Test names describe "how" instead of "what"
```go
// BAD: Bypass interface validation
func TestCreateUser_SavesToDatabase(t *testing.T) {
CreateUser(ctx, input)
row := db.QueryRow("SELECT * FROM users WHERE name = $1", "Alice")
// Direct database query, bypassing public interface
}
// GOOD: Validate through interface
func TestCreateUser_MakesUserRetrievable(t *testing.T) {
user, _ := CreateUser(ctx, input)
retrieved, _ := GetUser(ctx, user.ID)
assert.Equal(t, "Alice", retrieved.Name)
// Validate behavior through public interface
}
```
## Golang Testing Standards
### Test Naming
```go
// Test{Unit}_{Scenario}
func TestUserUsecase_CreateUser_Success(t *testing.T) {}
func TestUserUsecase_CreateUser_InvalidEmail(t *testing.T) {}
func TestUserUsecase_CreateUser_Duplicate(t *testing.T) {}
```
### Test Pyramid
```
/\
/ \
/ E2E \ <- Few critical flows
/--------\
/Integration\ <- API + DB
/--------------\
/ Unit Tests \ <- Most, 80%+ coverage
/--------------------\
```
### Mock Strategy
Only mock at **system boundaries**:
- External APIs (payments, email, etc.)
- Database (sometimes — prefer test DB)
- Time/randomness
- File system (sometimes)
Don't mock:
- Your own classes/modules
- Internal collaborators
- Things you can control
```go
// Use mockery to auto-generate mocks
//go:generate mockery --name=UserRepository
// Unit tests use mock repo
func TestUserUsecase_CreateUser_Success(t *testing.T) {
mockRepo := new(mock.UserRepository)
uc := NewUserUsecase(mockRepo, logger)
mockRepo.On("GetByEmail", mock.Anything, "test@example.com").Return(nil, nil)
mockRepo.On("Create", mock.Anything, mock.AnythingOfType("*domain.User")).Return(nil)
user, err := uc.CreateUser(context.Background(), input)
assert.NoError(t, err)
assert.NotNil(t, user)
}
```
## Interface Design for Testability
Good interfaces make testing natural:
**1. Accept dependencies, don't create them**
```go
// Testable
func (s *UserService) CreateUser(ctx context.Context, input CreateUserInput, repo UserRepository) (*User, error) {}
// Hard to test
func (s *UserService) CreateUser(ctx context.Context, input CreateUserInput) (*User, error) {
repo := postgres.NewUserRepository(db) // Creates dependency
}
```
**2. Return results, don't produce side effects**
```go
// Testable
func CalculateDiscount(cart *Cart) Discount {}
// Hard to test
func ApplyDiscount(cart *Cart) {
cart.Total -= discount // Mutates input
}
```
**3. Small interface surface area**
- Fewer methods = fewer tests to write
- Fewer parameters = simpler test setup
## Checklist for Each Cycle
```
[ ] Test describes behavior, not implementation
[ ] Test uses only public interfaces
[ ] Test still passes after internal refactoring
[ ] Code is minimal implementation to make current test pass
[ ] No speculative features
```
## Refactoring Candidates
After TDD cycle completes, look for:
- **Duplicate logic** → Extract function/class
- **Too long methods** → Split into private helpers (keep tests on public interface)
- **Shallow modules** → Merge or deepen
- **Feature envy** → Move logic to where the data is
- **Primitive obsession** → Introduce value objects
- **New code revealing existing code problems**
## Related Skills
- **Prerequisite**: `go-backend-dev` (used in implementation)
- **辅助**: `design-an-interface` (design interfaces for testability)
- **Follow-up**: `qa` (QA testing)