A lightweight mocking framework for Swift, powered by macros.
Annotate a protocol with @Mock and SwiftMocks generates a ready-to-use mock type — every
method and property implemented for you. No hand-written forwarding, no boilerplate spies.
Configure behaviour through a .stub surface and make assertions through a .verify surface.
@Mock
protocol Service {
func fetch(id: Int) async throws -> String
}
let service = ServiceMock()
service.stub.fetch { id in "loaded-\(id)" }
let value = try await service.fetch(id: 3) // "loaded-3"
#expect(service.verify.fetch.calledOnce)
#expect(service.verify.fetch.calledWith(.where { $0 == 3 }))- Swift 5.9+ (macros)
- macOS 10.15+ / iOS 13+ / tvOS 13+ / watchOS 6+
Add SwiftMocks to your Package.swift:
.package(url: "https://github.com/frugoman/SwiftMocks.git", from: "1.0.0")and depend on it from your test target:
.testTarget(name: "MyAppTests", dependencies: ["MyApp", "SwiftMocks"])Attach @Mock to a protocol. It generates a sibling type named <Protocol>Mock that conforms
to the protocol:
@Mock
protocol Service {
var name: String { get }
var retries: Int { get set }
func perform(with value: Int) -> String
func reset()
}
let service = ServiceMock() // conforms to ServiceEvery member forwards into a tracker that records calls and resolves stubs. You never write the forwarding yourself.
@Mock also works on a class: the generated mock subclasses it and overrides its members,
so a mocked call never reaches the real implementation. Use this when you can't extract a
protocol.
@Mock
class Repository {
func load(id: Int) -> String { realLoad(id) }
var count: Int { realCount() }
}
let repo = RepositoryMock() // is-a Repository
repo.stub.load { id in "mock-\(id)" }
repo.load(id: 1) // "mock-1" — the override, not realLoadBecause a mock must intercept every member, a class member that can't be overridden is a
compile-time error (rather than silently calling real code): final members, stored
properties, private members, and static members. Make the member computed/overridable, or
extract a protocol. Initializers are inherited, so the mock is constructed just like the class.
// A closure that receives the call's arguments
service.stub.perform { value in "got \(value)" }
// A fixed return value
service.stub.perform(returns: "fixed")
// A different value on each successive call (repeats the last once exhausted)
service.stub.perform(inSequence: ["a", "b", "c"])
// Conditional on the arguments — first matching stub wins, else the fallback above
service.stub.perform(when: .eq(42)) { _ in "forty-two" }For throwing members you can stub an error:
@Mock protocol Loader { func load() throws -> Data }
loader.stub.load(throws: MyError.notFound)Members returning Void, an Optional, or an empty Array / Dictionary / Set work as pure
spies — no stub required. Calling any other non-stubbed returning member reports a failure,
so a mock never silently runs unexpected behaviour.
verify.<member> exposes the call record:
service.perform(with: 7)
service.verify.perform.calledOnce // Bool
service.verify.perform.callsCount // Int
service.verify.perform.neverCalled // Bool
service.verify.perform.latestCall // 7
service.verify.perform.callsHistory // [7]Match arguments with a plain value (when Equatable) or a matcher:
service.verify.perform.calledWith(7) // exact value
service.verify.perform.calledWith(.any) // any value
service.verify.perform.calledWith(.where { $0 > 0 }) // predicateMulti-argument members match on the argument tuple:
@Mock protocol API { func send(id: Int, tag: String) }
api.verify.send.calledWith(.where { $0.0 == 1 && $0.1 == "x" })Overloaded methods share a name, so their tracker, stub, and verify accessors get a discriminator suffix derived from the parameter labels (or types, or return type). The conformance methods keep their normal overloaded signatures:
@Mock
protocol Sender {
func send(_ value: Int) -> String
func send(_ value: String) -> String
}
sender.stub.send_Int(returns: "int")
sender.stub.send_String(returns: "str")
sender.send(1) // "int"
sender.verify.send_Int.calledOnceA { get } property is stubbed and verified through its getter:
service.stub.name(returns: "Ada")
_ = service.name
service.verify.name.calledOnceA { get set } property also records assignments, verified via <name>Set:
service.stub.retries(returns: 0)
service.retries = 3
service.verify.retriesSet.calledWith(3) // trueA protocol may inherit from one other @Mock'd protocol. The generated mock subclasses the
base's mock, so it implements both sets of requirements and you stub / verify inherited members
through the same surfaces:
@Mock protocol Animal { func sound() -> String }
@Mock protocol Dog: Animal { func fetch() -> String }
let dog = DogMock()
dog.stub.sound(returns: "woof") // inherited member
dog.stub.fetch(returns: "stick") // own member
dog.verify.sound.calledOnce // inherited verification works too
let animal: Animal = dog // also usable as the base protocolThe base protocol must itself be annotated with @Mock. Inheriting from more than one
requirement-bearing protocol isn't supported (flatten the rest into one).
Every effect combination is supported, and a stub closure carries the same effects as the member:
@Mock
protocol Repository {
func value() async -> Int
func risky() throws -> Int
func fetch(id: Int) async throws -> String
}
repo.stub.value(returns: 1)
repo.stub.risky(throws: MyError.boom)
repo.stub.fetch { id in "row-\(id)" }@Mock targets protocols that declare their own instance methods and properties. The
following produce a clear compile-time diagnostic rather than a broken mock, and are on the
roadmap:
- inheriting from more than one requirement-bearing protocol
staticrequirements, initializers, subscripts, and associated types- throwing / async property accessors
inoutand variadic parameters- generic methods (a method-scoped generic can't be tracked at type scope)
Failures that originate inside a mock (such as calling an un-stubbed returning member) are routed
through SwiftMocks.failureReporter. The core library has no test-framework dependency; thin
adapter modules wire it to your framework — call the installer once (e.g. in test setup):
import SwiftMocksXCTest
SwiftMocks.useXCTest() // routes to XCTFail
import SwiftMocksTesting
SwiftMocks.useSwiftTesting() // routes to Issue.recordOr set it yourself:
SwiftMocks.failureReporter = { message, file, line in /* … */ }Apache 2.0 — see LICENSE.