Skip to Content
Course content

119: Mocking with Protocols

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I once worked with a developer who spent an entire Tuesday debugging a "flaky" test suite. Every time the CI pipeline ran, about 10% of the tests failed. It turned out the code was making actual network calls to a staging server to verify user authentication. When the staging server lagged or a network packet dropped, the tests failed. The developer was trying to fix the tests by adding sleep() calls to wait for the server, which is a nightmare—it just made the tests slower without actually fixing the instability.

The problem was that the business logic was tightly coupled to a concrete network class. To fix this, we have to stop our classes from caring how a task is done and instead make them care what can be done. In Swift, we do this through protocols.

Breaking the Concrete Link

When you instantiate a service directly inside a ViewModel or a Manager, you're creating a hard dependency. If your PaymentViewModel initializes a StripePaymentProcessor internally, you can't test that ViewModel without actually involving the Stripe SDK. That's a recipe for slow, brittle tests that require an internet connection.

Instead, we define a protocol that describes the interface. If the ViewModel only knows that it's talking to something that can process a payment, it doesn't matter if that something is a real API client or a fake object we created specifically for a test.

protocol PaymentProcessor {
    func processPayment(amount: Double, completion: @escaping (Bool) -> Void)
}

class StripePaymentProcessor: PaymentProcessor {
    func processPayment(amount: Double, completion: @escaping (Bool) -> Void) {
        // Actual complex network logic here
    }
}

class PaymentViewModel {
    private let processor: PaymentProcessor

    // We "inject" the dependency through the initializer
    init(processor: PaymentProcessor) {
        self.processor = processor
    }

    func pay(amount: Double) {
        processor.processPayment(amount: amount) { success in
            print(success ? "Paid!" : "Failed!")
        }
    }
}

Swapping Reality for Control

Now that the PaymentViewModel depends on the PaymentProcessor protocol rather than the StripePaymentProcessor class, we can create a "Mock." A mock is just a lightweight version of the service where we control the output. I usually add properties to my mocks to track whether a method was called or to force a specific return value.

This is where the magic happens. In your production code, you pass in the real Stripe service. In your test target, you pass in the mock. You no longer need to worry about API keys, network timeouts, or hitting rate limits during a test run.

class MockPaymentProcessor: PaymentProcessor {
    var processPaymentCalled = false
    var shouldSucceed = true

    func processPayment(amount: Double, completion: @escaping (Bool) -> Void) {
        processPaymentCalled = true
        completion(shouldSucceed)
    }
}

// In your test:
let mock = MockPaymentProcessor()
mock.shouldSucceed = false // Force a failure scenario
let viewModel = PaymentViewModel(processor: mock)

viewModel.pay(amount: 10.0)
assert(mock.processPaymentCalled == true)

I've found that this pattern—Dependency Injection via Protocols—is the single most effective way to reduce the "fear" of refactoring. When you have a suite of mocks, you can test every single edge case (like a 404 error or an empty JSON response) in milliseconds, rather than trying to manually trigger those errors on a real server.




📋 Practical Task

Exercise: Building a Mockable Weather Service

You are tasked with testing a WeatherDashboard class that displays the current temperature. Currently, it is tightly coupled to a RealWeatherAPI class that makes actual HTTP requests.

Your Goal: Refactor the code to use a protocol so you can test the dashboard without making network calls.

  • Create a protocol named WeatherProvider that defines a method: fetchTemperature(for city: String, completion: @escaping (Double?) -> Void).
  • Update the RealWeatherAPI class to conform to this protocol.
  • Modify the WeatherDashboard class to accept a WeatherProvider in its initializer (Dependency Injection).
  • Create a MockWeatherProvider that allows you to manually set a stubbedTemperature value.
  • Write a small test snippet (or print statements) that proves the WeatherDashboard correctly handles a scenario where the MockWeatherProvider returns nil (simulating a city not found).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.