Swift Mocking Patterns Using Protocols

Swift Mocking Patterns Using Protocols

Mocking in Swift revolves around one idea: hide concrete implementations behind protocols so tests can swap in controlled fakes. This guide covers three levels of sophistication—manual mocks, Mockingbird's generated mocks, and Sourcery templates—so you can choose the right tool for your project.

Why Protocols Enable Mocking

Swift doesn't have reflection-based mocking like Java's Mockito. Instead, you define a protocol that describes the interface your production code depends on, then inject either the real implementation or a test double:

// Protocol — the seam
protocol PaymentGateway {
    func charge(amount: Decimal, card: CreditCard) async throws -> ChargeResult
    func refund(chargeID: String) async throws
}

// Production implementation
final class StripeGateway: PaymentGateway { ... }

// Code under test accepts the protocol, not the concrete type
final class CheckoutService {
    private let gateway: PaymentGateway

    init(gateway: PaymentGateway) {
        self.gateway = gateway
    }

    func purchase(cart: Cart, card: CreditCard) async throws -> Order {
        let result = try await gateway.charge(amount: cart.total, card: card)
        return Order(chargeID: result.id, items: cart.items)
    }
}

Manual Protocol Mocks

For small interfaces, a hand-written mock is the fastest approach—no tooling required:

final class MockPaymentGateway: PaymentGateway {
    // Capture calls for assertions
    var chargeCallCount = 0
    var lastChargeAmount: Decimal?
    var lastChargeCard: CreditCard?

    // Stub results
    var chargeResult: ChargeResult = .init(id: "ch_test_123", status: .succeeded)
    var chargeShouldThrow: Error? = nil

    func charge(amount: Decimal, card: CreditCard) async throws -> ChargeResult {
        chargeCallCount += 1
        lastChargeAmount = amount
        lastChargeCard = card
        if let error = chargeShouldThrow { throw error }
        return chargeResult
    }

    var refundCallCount = 0
    var refundChargeIDs: [String] = []

    func refund(chargeID: String) async throws {
        refundCallCount += 1
        refundChargeIDs.append(chargeID)
    }
}

Tests use the mock directly:

func testPurchaseChargesCorrectAmount() async throws {
    let gateway = MockPaymentGateway()
    let service = CheckoutService(gateway: gateway)
    let cart = Cart(items: [.init(sku: "SKU-1", price: 29.99)])

    _ = try await service.purchase(cart: cart, card: .fixture)

    XCTAssertEqual(gateway.chargeCallCount, 1)
    XCTAssertEqual(gateway.lastChargeAmount, 29.99)
}

func testPurchaseThrowsWhenChargeFails() async {
    let gateway = MockPaymentGateway()
    gateway.chargeShouldThrow = PaymentError.cardDeclined
    let service = CheckoutService(gateway: gateway)

    await XCTAssertThrowsErrorAsync(
        try await service.purchase(cart: .fixture, card: .fixture)
    ) { error in
        XCTAssertEqual(error as? PaymentError, .cardDeclined)
    }
}

Manual mocks become tedious for large interfaces. That's where generators help.

Mockingbird

Mockingbird generates Swift mocks at build time via an Xcode build phase. The generated code mirrors Mockito's API.

Setup

brew install birdrides/mockingbird/mockingbird
cd MyProject
mockingbird install --project MyApp.xcodeproj --targets MyAppTests

Mockingbird adds a build phase that generates MockingbirdMocks.generated.swift before compilation.

Defining Mockable Protocols

// Mark the protocol — no extra annotation needed; Mockingbird scans all protocols
protocol AnalyticsService {
    func track(event: String, properties: [String: Any])
    func identify(userID: String)
}

Using Generated Mocks

import Mockingbird
@testable import MyApp

func testOnboardingTracksCompletedEvent() {
    let analytics = mock(AnalyticsService.self)
    let onboarding = OnboardingCoordinator(analytics: analytics)

    onboarding.complete()

    verify(analytics.track(event: "onboarding_completed", properties: any())).wasCalled()
}

func testOnboardingIdentifiesUser() {
    let analytics = mock(AnalyticsService.self)
    let onboarding = OnboardingCoordinator(analytics: analytics)

    onboarding.complete(userID: "user-42")

    verify(analytics.identify(userID: "user-42")).wasCalled(exactly(1))
}

Stubs work with given:

func testFeatureFlagEnabled() async throws {
    let flags = mock(FeatureFlagService.self)
    given(flags.isEnabled("new_checkout")).willReturn(true)

    let checkout = CheckoutViewController(flags: flags)
    XCTAssertTrue(checkout.usesNewFlow)
}

Mockingbird handles async protocol requirements and throws automatically.

Sourcery Auto-Generated Mocks

Sourcery is a general metaprogramming tool for Swift. The community-maintained AutoMockable template generates manual-style mocks from protocol annotations.

Installation

brew install sourcery

Add a sourcery.yml at your project root:

sources:
  - MyApp/Sources
templates:
  - .sourcery/AutoMockable.stencil
output: MyApp/Tests/Generated

Annotating Protocols

// sourcery: AutoMockable
protocol NotificationScheduler {
    func schedule(notification: LocalNotification) throws
    func cancelAll()
    var pendingCount: Int { get }
}

Run sourcery (or add it as an Xcode build phase) and it emits NotificationSchedulerMock:

// Generated by Sourcery
final class NotificationSchedulerMock: NotificationScheduler {
    var scheduleCallsCount = 0
    var scheduleCalled: Bool { scheduleCallsCount > 0 }
    var scheduleReceivedNotification: LocalNotification?
    var scheduleThrowableError: Error?
    var scheduleClosure: ((LocalNotification) throws -> Void)?

    func schedule(notification: LocalNotification) throws {
        scheduleCallsCount += 1
        scheduleReceivedNotification = notification
        if let error = scheduleThrowableError { throw error }
        try scheduleClosure?(notification)
    }

    var cancelAllCallsCount = 0
    func cancelAll() { cancelAllCallsCount += 1 }

    var pendingCount: Int = 0
}

The generated class follows the same pattern as manual mocks but is always in sync with the protocol definition.

Using Sourcery Mocks

func testReminderSchedulesNotification() throws {
    let scheduler = NotificationSchedulerMock()
    let reminder = ReminderService(scheduler: scheduler)

    try reminder.scheduleDaily(hour: 9, message: "Check your tasks")

    XCTAssertTrue(scheduler.scheduleCalled)
    XCTAssertEqual(scheduler.scheduleReceivedNotification?.hour, 9)
}

func testScheduleErrorPropagates() {
    let scheduler = NotificationSchedulerMock()
    scheduler.scheduleThrowableError = NotificationError.permissionDenied
    let reminder = ReminderService(scheduler: scheduler)

    XCTAssertThrowsError(try reminder.scheduleDaily(hour: 9, message: "Test")) { error in
        XCTAssertEqual(error as? NotificationError, .permissionDenied)
    }
}

Choosing the Right Approach

Scenario Tool
1–2 methods, changes rarely Manual mock
Large interface, many tests, Xcode project Mockingbird
Swift Package or CI-first project Sourcery
Async-heavy protocols All three work; Mockingbird has best ergonomics

Key Takeaways

  • Protocols are the mocking seam in Swift. Design dependencies against protocols from the start.
  • Manual mocks are zero-dependency and easy to understand; maintain them by hand for small interfaces.
  • Mockingbird generates verify / given syntax at build time; great for Xcode-centric projects.
  • Sourcery generates conservative, manual-style mocks and works equally well in Swift packages.

Clean protocol-based mocks make your unit tests fast, deterministic, and focused on behaviour. Once your suite is solid, HelpMeTest can run it continuously and alert you to regressions before they reach users.

Start now free