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 MyAppTestsMockingbird 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 sourceryAdd a sourcery.yml at your project root:
sources:
- MyApp/Sources
templates:
- .sourcery/AutoMockable.stencil
output: MyApp/Tests/GeneratedAnnotating 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/givensyntax 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.