swift-mockable provides a @Mockable macro that generates protocol mocks for tests.
- Generated mocks are emitted inside
#if DEBUG. - Generated names follow a predictable convention (
<name>CallCount,<name>CallArgs,<name>Handler). resetMock()is generated to clear all tracking state.
Add the package:
dependencies: [
.package(url: "https://github.com/yysskk/swift-mockable.git", from: "1.9.1")
]Add Mockable to your target:
.target(
name: "YourTarget",
dependencies: ["Mockable"]
)Note
The first time you build a target that uses @Mockable, Xcode shows a
"trust macro" prompt. Choose Trust & Enable to allow the macro to run.
On the command line, swift build runs macros without prompting.
import Mockable
@Mockable
protocol UserService {
func fetchUser(id: Int) async throws -> User
func saveUser(_ user: User) async throws
var currentUser: User? { get }
var isLoggedIn: Bool { get set }
}
let mock = UserServiceMock()
mock.fetchUserHandler = { id in
User(id: id, name: "Test User")
}
mock._currentUser = User(id: 1, name: "Current")
mock.isLoggedIn = true
let user = try await mock.fetchUser(id: 42)
#expect(user.id == 42)
#expect(mock.fetchUserCallCount == 1)
#expect(mock.fetchUserCallArgs == [42])
mock.resetMock()
#expect(mock.fetchUserCallCount == 0)For each protocol requirement, @Mockable generates test-friendly members:
- Functions:
<method>CallCount<method>CallArgs<method>Handler
- Properties:
- Backing storage for setup (for example
_<property>) - Computed protocol-conforming property (
property)
- Backing storage for setup (for example
- Subscripts:
subscript<suffix>CallCountsubscript<suffix>CallArgssubscript<suffix>Handlersubscript<suffix>SetHandlerfor get/set subscripts
- Utility:
resetMock()
A handler for a member with two or more parameters takes individual parameters, so it
can be written as { a, b in ... } (no tuple destructuring needed):
@Mockable
protocol Calculator {
func add(a: Int, b: Int) -> Int
}
// var addHandler: (@Sendable (Int, Int) -> Int)? = nil
mock.addHandler = { a, b in a + b }Notes:
- This applies to methods and subscripts alike (subscript getter
(Int, Int) -> V, setter(Int, Int, V) -> Void). - Zero- and single-parameter members pass their argument directly.
<name>CallArgsis a labeled tuple array (e.g.[(a: Int, b: Int)]) — the call history keeps parameter labels even though the handler takes individual parameters.
- Access-level-aware generation (including
private/fileprivateedge cases) - Sync /
async/throws/rethrowsmethods - Typed throws (
throws(MyError), SE-0413) on methods, properties, and subscripts - Variadic parameters (captured as arrays)
@autoclosureparameters (evaluated once per call; handlers andCallArgsreceive the evaluated value)- Non-escaping closure parameters (forwarded to the handler; excluded from
CallArgs) inoutparameters with write-back support- Generic methods (generic parameters are type-erased to
Anyin storage/handlers) - Overloaded methods (unique suffixes are added to generated names when needed)
- Associated types (generated as
typealias, using default type when available, otherwiseAny) - Static methods and static properties
- Get-only / get-set / optional properties
- Effectful read-only properties (
get async,get throws,get async throws) mocked with handlers - Get-only / get-set subscripts (including effectful
get async/get throwssubscripts) #if/#elseif/#elseconditional compilation inside protocols- Protocol inheritance (child mock inherits from first parent mock when applicable)
Sendableprotocol support (@unchecked Sendablemock generation)Actorprotocol support (actor mock generation with nonisolated helper members)
- Return-value methods and get-only subscripts return a default when their handler is not set if the return type has one: Optionals return
nil, arrays and sets return an empty collection, and dictionaries return an empty dictionary. Any other return type callsfatalError. - Properties with effectful getters (
get async/get throws) generate<name>CallCountand<name>Handlerinstead of_<name>backing storage; the same unset-handler defaults apply. - Void-return methods and subscript setters are no-op when handler is
nil. @autoclosurearguments are evaluated exactly once per call (even when no handler is set); if evaluating a throwing autoclosure throws, the error propagates before the call is recorded.- Non-escaping closure arguments are forwarded to the handler but excluded from
CallArgs(a non-escaping value cannot be stored); the call is still counted. rethrowsmethods generate a non-throwing handler that receives the throwing closure arguments (a stored handler cannot satisfyrethrowson its own). The handler decides whether to invoke those closures; the mock itself does not re-throw their errors.- Typed throws (
throws(MyError)) keeps thethrows(MyError)signature and generates a plain untyped-throwing handler; the body re-throws the handler's error as the declared type. Configure the handler normally (mock.loadHandler = { id in throw MyError() }). If the handler throws a different error type, the mock traps. resetMock()clears handlers, call counts, call arguments, and backing properties.- For inherited protocols,
resetMock()callssuper.resetMock()before resetting child members.
@Mockablecan only be applied to protocols.@Mockabledoes not accept arguments.- Unsupported protocol members (for example
init) emit compile-time diagnostics. - Static/class subscripts are not supported.
- For protocols with multiple parent protocols, the first parent is used as the mock superclass.
- The
<Protocol>Mocktype can't be found. The generated mock lives inside#if DEBUG, so it only exists in debug builds. Reference it from test targets or debug configurations. - "Macro expansion" / trust prompt in Xcode. Choose Trust & Enable the
first time you build a target that uses
@Mockable(see the note in Installation). - A handler is required. A return-value method or get-only subscript with an
unset handler calls
fatalError, unless the return type has a natural empty value (see Behavioral Notes). Set the corresponding<name>Handlerin your test setup. - Overloaded calls need a type annotation. For methods overloaded only by
return type, annotate the result (e.g.
let value: String = mock.get(...)) so Swift selects the right overload.
- Swift 5.9, 5.10, and 6.2+
- macOS 10.15+ / iOS 13+ / tvOS 13+ / watchOS 6+
MockableLocklock strategy:- iOS 18.0+ / macOS 15.0+ / tvOS 18.0+ / watchOS 11.0+: prefers
Mutex(Synchronization) - Older OS versions: falls back to
LegacyLock(NSLock-based)
- iOS 18.0+ / macOS 15.0+ / tvOS 18.0+ / watchOS 11.0+: prefers
MIT