Introduction to Fakes in TDD

Welcome to our lesson on using Fakes as test doubles in Test-Driven Development (TDD) with Swift and XCTest. In this lesson, you'll explore how fakes can streamline your testing by simulating real-world components. Our journey so far has exposed you to various test doubles like dummies, stubs, spies, and mocks. Now, we'll dive into fakes, which enable you to create realistic implementations that mirror complex dependencies, making your tests more robust and reliable. As always, we'll practice the TDD cycle: Red, Green, Refactor, as we see how fakes fit into our testing strategy.

Code Example and Walkthrough: Implementing an In-memory Fake Repository

Let's see how to implement a simple fake: an InMemoryUserRepository. This serves as a stand-in for a real database repository, providing controlled behavior for our tests.

Create InMemoryUserRepository.swift:

import Foundation

struct User {
    let id: String
    let email: String
    let name: String
    let createdAt: Date
}

class InMemoryUserRepository {
    private var users: [String: User] = [:]
    private var currentId = 1

    func generateId() -> String {
        defer { currentId += 1 }
        return String(currentId)
    }

    func create(userData: [String: Any]) -> User? {
        guard let email = userData["email"] as? String,
              let name = userData["name"] as? String else { return nil }
        
        let user = User(
            id: generateId(),
            email: email,
            name: name,
            createdAt: Date()
        )
        users[user.id] = user
        return user
    }

    func findById(_ id: String) -> User? {
        return users[id]
    }

    func findByEmail(_ email: String) -> User? {
        return users.values.first { $0.email == email }
    }

    func update(id: String, data: [String: Any]) -> User? {
        guard let existing = users[id] else { return nil }
        
        let updated = User(
            id: existing.id,
            email: data["email"] as? String ?? existing.email,
            name: data["name"] as? String ?? existing.name,
            createdAt: existing.createdAt
        )
        users[id] = updated
        return updated
    }

    func delete(id: String) -> Bool {
        return users.removeValue(forKey: id) != nil
    }

    func findAll() -> [User] {
        return Array(users.values)
    }

    // Helper method for testing
    func clear() {
        users.removeAll()
        currentId = 1
    }
}

Explanation:

  • We create in-memory storage for users using a dictionary.
  • Each function simulates typical database operations like create, findById, and findAll.
  • The clear method ensures data isolation between tests, a crucial feature for repeatable outcomes.

By having a controlled data store, we make sure our tests are focused on business logic and not dependent on an external database. Fakes are often quite complicated (compared to Mocks or Stubs) to build because they mimic the behavior of the real thing. They can be used to verify the state after your code acts on the fake, which can be really useful when you are trying to mimic the environment as best as possible without introducing the uncertainty or delay that the real implementation would introduce.

Unlike stubs or mocks, which are often minimal and tailored to a specific test case, fakes tend to encapsulate actual logic and can be reused across multiple tests or even test suites. Because of this, fakes begin to resemble real implementations and should be treated with similar care in terms of correctness, consistency, and isolation from production behavior.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal