Skip to content
intermediate Phase 10 · Testing & Debugging

Test-Driven Development

Apply TDD workflow: red-green-refactor cycle for building features with confidence.

50m
3 problems
Topic Progress 0%

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

  1. Red: Write a small test for the next bit of functionality. Run it. It fails because the code does not exist yet.
  2. Green: Write the simplest possible code to make the test pass. Do not worry about elegance yet.
  3. 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?

Question 1 options

2. In TDD, what should you do during the Green phase?

Question 2 options

3. Which iOS component is best suited for TDD?

Question 3 options

4. What is a key benefit of writing tests first?

Question 4 options

Flashcards

Question

What does Red-Green-Refactor mean?

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?

Answer

Writing only what the test requires prevents over-engineering. Elegance and optimization happen during the Refactor phase.

Question

When is TDD least effective?

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

  1. Red: Write failing test
  2. Green: Minimal code to pass
  3. Refactor: Clean up
  • One behavior per test
  • Descriptive test names
  • Keep tests fast and independent
  • Best for: ViewModels, validation, business logic