| name | testing-tdd-feature |
| description | Red-green-refactor scaffold for building new features with TDD. Write failing tests first, then implement to pass. Use when building new features test-first. |
First step: Tell the user: "testing-tdd-feature skill loaded."
TDD Feature
Build new features using the red-green-refactor cycle. Tests define the spec, AI generates the implementation, tests verify correctness.
When This Skill Activates
Use this skill when the user:
- Wants to "TDD a new feature" or "build test-first"
- Says "I want tests before code"
- Asks for "red-green-refactor" workflow
- Wants AI to generate code that's provably correct
- Is building a new module, service, or feature from scratch
Why TDD for New Features with AI
Traditional: AI generates code → You hope it's correct → Ship → Find bugs
TDD with AI: You write tests (spec) → AI generates code to pass → Proven correct
The test is your acceptance criteria in code form. AI excels at going from failing test to passing implementation — it's a concrete, unambiguous target.
Process
Phase 1: Define the Feature
Before writing any code or tests, understand:
- What does this feature do? (user story or requirement)
- What are the inputs? (parameters, user actions, data)
- What are the outputs? (return values, state changes, UI updates)
- What are the edge cases? (empty, nil, error, boundary)
- What dependencies does it need? (network, storage, other services)
Phase 2: Design the API Surface
Sketch the public interface before writing tests:
protocol FavoriteManaging {
func add(_ item: Item) async throws
func remove(_ item: Item) async throws
func isFavorite(_ item: Item) -> Bool
var favorites: [Item] { get }
var count: Int { get }
}
This doesn't need to compile yet — it's the contract you'll test against.
Phase 3: RED — Write Failing Tests
Write tests for each behavior. Start with the simplest case and build up.
Order of Tests (Simple → Complex)
- Construction — can you create the object?
- Happy path — does the basic operation work?
- State verification — does state update correctly?
- Edge cases — empty, nil, boundaries
- Error handling — what fails and how?
- Integration — does it work with dependencies?
Template: Feature Test Suite
import Testing
@testable import YourApp
@Suite("FavoriteManager")
struct FavoriteManagerTests {
@Test("starts with empty favorites")
func startsEmpty() {
let manager = FavoriteManager()
#expect(manager.favorites.isEmpty)
#expect(manager.count == 0)
}
@Test("can add a favorite")
func addFavorite() async throws {
let manager = FavoriteManager()
let item = Item(id: "1", title: "Test")
try await manager.add(item)
#expect(manager.count == 1)
#expect(manager.isFavorite(item))
}
@Test("can remove a favorite")
func removeFavorite() async throws {
let manager = FavoriteManager()
let item = Item(id: "1", title: )
manager.add(item)
manager.remove(item)
#expect(manager.count )
#expect(manager.isFavorite(item))
}
()
() {
manager ()
item (id: , title: )
manager.add(item)
manager.add(item)
#expect(manager.count )
}
()
() {
manager ()
item (id: , title: )
manager.remove(item)
#expect(manager.count )
}
()
() {
manager (maxCapacity: )
items ().map { (id: , title: ) }
#expect(throws: .capacityExceeded) {
item items {
manager.add(item)
}
}
}
()
() {
manager ()
items [, , ].map { (id: , title: ) }
item items {
manager.add(item)
}
#expect(manager.favorites.map(\.title) [, , ])
}
}
Run tests — they should ALL fail (the type doesn't even exist yet).
Phase 4: GREEN — Implement to Pass
Now implement the feature. Pass the tests as context to AI:
Prompt to Claude: "Here are my failing tests for FavoriteManager.
Implement the FavoriteManager class to make all tests pass.
Follow the protocol FavoriteManaging."
Implementation Rules
- One test at a time — make the first test pass, then the second, etc.
- Write the simplest code that passes each test
- Don't anticipate future tests — only satisfy current failing tests
- Run tests after each change
xcodebuild test -scheme YourApp \
-only-testing "YourAppTests/FavoriteManagerTests"
Phase 5: REFACTOR
With all tests green, clean up the implementation:
- Extract helper methods
- Improve naming
- Remove duplication
- Optimize performance (if tests cover perf requirements)
Run tests after every refactor step. If any test fails, you've changed behavior — revert.
Phase 6: Integration
Once the unit is solid, write integration tests:
@Suite("FavoriteManager Integration")
struct FavoriteManagerIntegrationTests {
@Test("persists favorites across sessions")
func persistence() async throws {
let store = InMemoryStore()
let manager1 = FavoriteManager(store: store)
try await manager1.add(Item(id: "1", title: "Test"))
let manager2 = FavoriteManager(store: store)
await manager2.loadFavorites()
#expect(manager2.count == 1)
}
}
TDD Rhythm
RED → Write one failing test (30 seconds - 2 minutes)
GREEN → Make it pass with simplest code (1 - 5 minutes)
REFACTOR → Clean up while tests stay green (1 - 3 minutes)
REPEAT → Next test
Cadence matters. If you're spending more than 5 minutes on GREEN, the test might be too big. Break it into smaller tests.
Test Categories by Feature Type
ViewModel Feature
@Suite("SearchViewModel")
struct SearchViewModelTests {
@Test("starts in idle state")
@Test("searching updates state to loading")
@Test("successful search shows results")
@Test("empty search shows empty state")
@Test("failed search shows error")
@Test("debounces rapid input")
@Test("cancels previous search on new input")
}
Data Layer Feature
@Suite("ItemRepository")
struct ItemRepositoryTests {
@Test("fetches items from remote")
@Test("caches fetched items locally")
@Test("returns cached items when offline")
@Test("syncs local changes to remote")
@Test("handles conflict resolution")
@Test("deletes expire cached items")
}
Business Logic Feature
@Suite("SubscriptionManager")
struct SubscriptionManagerTests {
@Test("free user has basic access")
@Test("pro user has full access")
@Test("expired subscription reverts to free")
@Test("family member inherits subscription")
@Test("trial period grants pro access")
@Test("grace period maintains access after lapse")
}
Output Format
## TDD Feature: [Feature Name]
### API Design
```swift
// Protocol / public interface
Tests Written (RED)
startsEmpty — Initial state
addFavorite — Happy path
removeFavorite — State change
addDuplicate — Edge case
removeNonExistent — Edge case
storageFullError — Error handling
Implementation (GREEN)
File: Sources/Features/FavoriteManager.swift
All [X] tests passing.
Refactoring Done
- Extracted storage logic to private method
- Renamed internal property for clarity
Next Steps
## Common Pitfalls
| Pitfall | Problem | Solution |
|---------|---------|----------|
| Writing too many tests before implementing | Overwhelming; can't see progress | Write 2-3 tests, implement, repeat |
| Tests that test implementation | Brittle; break on refactor | Test behavior and outcomes only |
| Skipping the refactor step | Accumulating technical debt | Refactor every 3-5 green cycles |
| AI implementing beyond the tests | Untested code in production | Only implement what tests require |
| Not running tests after each change | Silent regressions | `xcodebuild test` after every edit |
## References
- Kent Beck, *Test-Driven Development: By Example*
- `testing-test-contract/` — for protocol-level test suites
- `testing-test-data-factory/` — for reducing test setup boilerplate
- `generators-test-generator/` — for standalone test generation