Red-Green-Refactor Cycle
What is TDD?
Test-Driven Development is a software development process where you write a failing test before writing the production code that makes it pass. The cycle is: Red (write a failing test), Green (write minimal code to pass), Refactor (clean up while keeping tests green).
The TDD Mantra
- Red: Write a small test for the next bit of functionality. Run it. It fails because the code does not exist yet.
- Green: Write the simplest possible code to make the test pass. Do not worry about elegance yet.
- Refactor: Clean up the code while ensuring all tests still pass. Remove duplication, improve naming, extract methods.
Why TDD Works
- Forces you to think about requirements before implementation
- Produces a comprehensive test suite as a byproduct
- Encourages small, focused units of code
- Prevents over-engineering since you only add code to make tests pass
- Provides confidence to refactor and add features
TDD vs Traditional Development
In traditional development, you write code then tests. This often leads to tests that validate existing behavior rather than desired behavior. With TDD, the test defines the desired behavior first, and the implementation follows.
When to Use TDD
TDD is most effective for:
- Business logic and domain models
- ViewModels and presentation logic
- Data transformation and validation
- Algorithm implementations
- API response parsing
TDD is less effective for:
- UI layout and visual design
- Boilerplate code
- Simple CRUD operations
- Third-party library integration
TDD Workflow in Practice
Step-by-Step Example: Shopping Cart
Let us build a shopping cart using TDD. We start with the simplest behavior and grow the solution incrementally.
Step 1: Write the first failing test
class ShoppingCartTests: XCTestCase {
func test_emptyCart_hasZeroItems() {
let cart = ShoppingCart()
XCTAssertEqual(cart.itemCount, 0)
}
}
This fails because ShoppingCart does not exist. Create a minimal stub:
struct ShoppingCart {
var itemCount: Int { 0 }
}
Test passes. Refactor: the implementation is already minimal.
Step 2: Add an item
func test_addItem_increasesItemCount() {
var cart = ShoppingCart()
cart.add(Product(name: "Apple", price: 1.0))
XCTAssertEqual(cart.itemCount, 1)
}
Fails. Implement:
struct Product: Equatable {
let name: String
let price: Decimal
}
struct ShoppingCart {
private var items: [Product] = []
var itemCount: Int { items.count }
mutating func add(_ product: Product) {
items.append(product)
}
}
Step 3: Calculate total
func test_total_isSumOfProductPrices() {
var cart = ShoppingCart()
cart.add(Product(name: "Apple", price: 1.50))
cart.add(Product(name: "Banana", price: 0.75))
XCTAssertEqual(cart.total, 2.25)
}
Implement:
var total: Decimal {
items.reduce(0) { $0 + $1.price }
}
Step 4: Remove an item
func test_removeItem_decreasesItemCount() {
var cart = ShoppingCart()
let apple = Product(name: "Apple", price: 1.0)
cart.add(apple)
cart.remove(apple)
XCTAssertEqual(cart.itemCount, 0)
}
Each cycle adds exactly one behavior. The code grows organically with full test coverage.
TDD for iOS Features
TDD with ViewModels
ViewModels are the sweet spot for TDD in iOS. They contain presentation logic that can be tested without UI:
// Test first
func test_searchFiltersUsersByName() async {
let viewModel = SearchViewModel(
repository: FakeUserRepository(users: [
User(name: "Alice"),
User(name: "Bob"),
User(name: "Alice Smith")
])
)
await viewModel.search(query: "Alice")
XCTAssertEqual(viewModel.results.count, 2)
}
// Then implement
@MainActor
class SearchViewModel: ObservableObject {
@Published var results: [User] = []
private let repository: UserRepository
init(repository: UserRepository) {
self.repository = repository
}
func search(query: String) async {
let allUsers = try? await repository.fetchUsers()
results = allUsers?.filter {
$0.name.localizedCaseInsensitiveContains(query)
} ?? []
}
}
TDD for Validation Logic
// Test
func test_validateEmail_rejectsInvalidFormats() {
let validator = EmailValidator()
XCTAssertFalse(validator.isValid("notanemail"))
XCTAssertFalse(validator.isValid("@example.com"))
XCTAssertFalse(validator.isValid("user@"))
XCTAssertTrue(validator.isValid("user@example.com"))
}
// Implement
struct EmailValidator {
func isValid(_ email: String) -> Bool {
let pattern = "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$"
return email.range(of: pattern, options: .regularExpression) != nil
}
}
TDD for Navigation
func test_navigateToDetail_passesCorrectItem() {
let router = AppRouter()
let item = Item(id: 42, name: "Test")
router.navigate(to: .detail(item))
if case .detail(let selected) = router.currentRoute {
XCTAssertEqual(selected.id, 42)
} else {
XCTFail("Expected detail route")
}
}
Tips for Effective TDD
- Start with the simplest test case
- One assertion per test when possible
- Do not write production code without a failing test
- Keep refactoring separate from feature additions
- Use descriptive test names that document behavior
- Delete tests that no longer provide value
Quiz
1. What is the correct order of the TDD cycle?
2. In TDD, what should you do during the Green phase?
3. Which iOS component is best suited for TDD?
4. What is a key benefit of writing tests first?
Flashcards
Question
What does Red-Green-Refactor mean?
Click to reveal answer
Answer
Red: write a failing test. Green: write minimal code to pass. Refactor: clean up code while tests stay green.
Question
Why should you write the simplest code during Green?
Click to reveal answer
Answer
Writing only what the test requires prevents over-engineering. Elegance and optimization happen during the Refactor phase.
Question
When is TDD least effective?
Click to reveal answer
Answer
For UI layout, visual design, boilerplate code, and third-party library integration where tests provide less value.
Revision Notes
Key Takeaways
- 1. Never write production code without a failing test first
- 2. Keep the Green phase minimal - only make the test pass
- 3. Refactor only when all tests are green
- 4. TDD produces natural documentation through test names
- 5. Start with the simplest case and grow complexity incrementally
Interview Tips
- • Walk through a TDD example from Red to Green to Refactor
- • Explain how TDD leads to better architecture
- • Describe when you would NOT use TDD
- • Discuss how TDD helps with code review and documentation
Cheat Sheet
TDD Quick Reference
- Red: Write failing test
- Green: Minimal code to pass
- Refactor: Clean up
- One behavior per test
- Descriptive test names
- Keep tests fast and independent
- Best for: ViewModels, validation, business logic