Skill v1.0.1
currentAutomated scan100/1001 files
version: "1.0.1" name: 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. allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion]
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 bugsTDD 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:
// Example: Designing a FavoriteManagerprotocol FavoriteManaging {func add(_ item: Item) async throwsfunc remove(_ item: Item) async throwsfunc isFavorite(_ item: Item) -> Boolvar 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 {// 1. Construction@Test("starts with empty favorites")func startsEmpty() {let manager = FavoriteManager()#expect(manager.favorites.isEmpty)#expect(manager.count == 0)}// 2. Happy path@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))}// 3. State verification@Test("can remove a favorite")func removeFavorite() async throws {let manager = FavoriteManager()let item = Item(id: "1", title: "Test")try await manager.add(item)try await manager.remove(item)#expect(manager.count == 0)#expect(!manager.isFavorite(item))}// 4. Edge cases@Test("adding duplicate does not increase count")func addDuplicate() async throws {let manager = FavoriteManager()let item = Item(id: "1", title: "Test")try await manager.add(item)try await manager.add(item)#expect(manager.count == 1)}@Test("removing non-existent item does nothing")func removeNonExistent() async throws {let manager = FavoriteManager()let item = Item(id: "1", title: "Test")try await manager.remove(item)#expect(manager.count == 0)}// 5. Error handling@Test("throws when storage is full")func storageFullError() async {let manager = FavoriteManager(maxCapacity: 2)let items = (1...3).map { Item(id: "\($0)", title: "Item \($0)") }await #expect(throws: FavoriteError.capacityExceeded) {for item in items {try await manager.add(item)}}}// 6. Ordering@Test("favorites are in insertion order")func insertionOrder() async throws {let manager = FavoriteManager()let items = ["C", "A", "B"].map { Item(id: $0, title: $0) }for item in items {try await manager.add(item)}#expect(manager.favorites.map(\.title) == ["C", "A", "B"])}}
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()// Session 1: Add favoritelet manager1 = FavoriteManager(store: store)try await manager1.add(Item(id: "1", title: "Test"))// Session 2: Verify it persistslet 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
// Protocol / public interface
### Tests Written (RED)1. `startsEmpty` — Initial state2. `addFavorite` — Happy path3. `removeFavorite` — State change4. `addDuplicate` — Edge case5. `removeNonExistent` — Edge case6. `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- [ ] Add integration tests with persistence- [ ] Wire up to ViewModel- [ ] Add UI for favorite toggle
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 suitestesting/test-data-factory/— for reducing test setup boilerplategenerators/test-generator/— for standalone test generation