This guide explains generated naming, edge-case behavior, and constraints of @Mockable.
For a non-overloaded method fetch, generated members are:
fetchCallCountfetchCallArgsfetchHandler
For overloaded methods, a suffix is appended:
- Start with sanitized parameter type names.
- If that still collides, append:
- return type (if non-
Void) Async(forasync)Throwing(forthrows)
- return type (if non-
Example:
func get(url: URL) async -> String
func get(url: URL) async throws -> DataGenerates distinct handlers like:
getURLStringAsyncHandlergetURLDataAsyncThrowingHandler
A suffixed name can also spell out a name another requirement already uses. Identifiers are assigned across the whole protocol, so the requirement tracked under a name it declares — a method, or a property — keeps it, and the suffixed one continues counting:
@Mockable
protocol Service {
func load()
func load(_ item: Item) // loadItem is taken -> loadItem2CallCount, loadItem2Handler, ...
func loadItem() // loadItemCallCount, loadItemHandler, ...
}This holds whichever requirement is declared first, and applies to subscripts and initializers alike.
Subscript-generated names use subscript<suffix>....
The suffix is based on subscript parameter types.
Example:
subscript(index: Int) -> String { get }Generates:
subscriptIntCallCountsubscriptIntCallArgssubscriptIntHandler
Get/set subscripts also generate subscript<suffix>SetHandler.
A sole init requirement generates members based on the identifier init:
initCallCountinitCallArgs
Overloaded initializers append a parameter-type suffix, matching the method scheme:
init(host: String)
init(host: String, port: Int)Generates:
initStringCallCount/initStringCallArgsinitStringIntCallCount/initStringIntCallArgs
When method signatures contain generic type parameters:
CallArgsstorage uses type erasure (Any) where needed.- Handler parameter/return types are also erased where needed.
- Generated method implementations cast generic returns back to the requested type.
This keeps generated mocks concrete while preserving call tracking.
A generic parameter nested in a collection is erased in place, so the erased type keeps
its shape ([T] becomes [Any], [String: T] becomes [String: Any]):
func transform<T>(_ map: [String: T]) -> [String: T]
// generates:
// var transformCallArgs: [[String: Any]] = []
// var transformHandler: (@Sendable ([String: Any]) -> [String: Any])? = nilA dictionary whose key mentions a generic parameter is erased as a whole ([T: String]
becomes Any), because Any is not Hashable.
Any other type that mentions a generic parameter is erased to Any: a bare parameter
(T), a generic type applied to one (UserDefaultsKey<T>, Box<[T]>), a qualified
spelling of either (MyModule.Box<T>, Swift.Array<T>), a type nested in a parameter
(T.Element), an existential (any Sequence<T>), and a metatype (T.Type). Such a type
cannot be erased in place the way the sugared collections are: rewriting Box<T> to
Box<Any> would require Box to accept Any, which its own generic constraints may
forbid.
A closure that mentions a generic parameter is erased like any other type, and keeps its
parentheses when it is nested in an optional: (() -> T)? becomes (() -> Any)?.
Two closure positions cannot be mocked and emit a diagnostic:
- a return type that mentions a generic parameter inside a function type, such as
func makeSetter<T>() -> (T) -> Void. The mock casts the erased handler result back to the declared type, and Swift cannot convert between function types at runtime. - a closure parameter whose own parameters mention a generic parameter, such as
func observe<T>(_ handler: (T) -> Void). A closure's parameters are contravariant, so(T) -> Voidcannot be passed where(Any) -> Voidis expected. Erasing a closure's result is fine, sofunc load<T>(_ make: () -> T)is mocked normally.
A function type reached through another type (Box<() -> T>) is unaffected in either
position, because that type is erased, forwarded, and cast back as a whole.
Each associated type generates a typealias in the mock:
- If the protocol provides a default associated type, that type is used.
- Otherwise,
Anyis used.
Example:
associatedtype Value = IntGenerates:
typealias Value = IntWithout default:
associatedtype ValueGenerates:
typealias Value = AnyAn associated type carrying a constraint — a conformance or a where clause — needs a
default, because Any does not satisfy it and the mock would not conform. Declaring one
without a default emits a diagnostic:
associatedtype Item: Decodable // reported: give it a default
associatedtype Item: Decodable = Data // mocked as `typealias Item = Data`A typealias the protocol declares is re-emitted on the mock with its generic parameter
and where clauses intact.
@autoclosure arguments are evaluated exactly once per call, before the call is
recorded. CallArgs and handlers observe the evaluated value, not the closure:
func log(_ message: @autoclosure () -> String)Generates:
var logCallArgs: [String] = []
var logHandler: (@Sendable (String) -> Void)? = nilNotes:
- The argument is evaluated even when no handler is set, so the call can be recorded.
- If evaluating a throwing autoclosure throws, the error propagates before the call
is recorded (
CallCountis not incremented). - An autoclosure's own effects must be covered by the requirement: a throwing
autoclosure requires a
throwsrequirement and an async autoclosure requires anasyncrequirement; otherwise a compile-time diagnostic is emitted. Effectful autoclosures are not supported in subscript requirements.
A non-escaping closure parameter cannot be stored, so it is excluded from
CallArgs. The call is still counted, and the closure is still forwarded to the
handler:
func run(label: String, _ body: () -> Void)Generates:
var runCallArgs: [String] = [] // only the storable `label`
var runHandler: (@Sendable (String, () -> Void) -> Void)? = nilEscaping (@escaping), optional, and variadic closures are storable and remain
in CallArgs as before.
A stored handler cannot satisfy a rethrows requirement on its own — a rethrows
body may only throw through the requirement's own closure parameters. The mock
therefore keeps the rethrows signature but generates a non-throwing handler
that receives those closures:
func run(_ body: () throws -> Void) rethrows
// generates:
// var runHandler: (@Sendable (() throws -> Void) -> Void)? = nil
// func run(_ body: () throws -> Void) rethrows { ... _handler(body) ... }The handler receives the throwing closures and decides whether to invoke them. Because the handler is non-throwing, the mock does not itself re-throw their errors; verify behavior through the handler and the call count.
Typed throws (throws(MyError)) on methods, effectful properties, and effectful
subscripts is supported. The mock keeps the throws(MyError) signature, but the
handler is a plain untyped-throwing closure and the generated body re-throws its
error as the requirement's type:
func load(id: Int) throws(LoadError) -> String
// generates:
// var loadHandler: (@Sendable (Int) throws -> String)? = nil
// func load(id: Int) throws(LoadError) -> String {
// ...
// do { return try _handler(id) } catch { throw error as! LoadError }
// }Configure the handler as usual — no error-type annotation needed:
mock.loadHandler = { id in throw LoadError() }This keeps the package's full deployment range (an untyped handler avoids the
Swift 6 runtime requirement for typed-throws function values) and supports
generic error types (func run<E: Error>(_ body: () throws(E) -> Void) throws(E)).
If a handler throws an error of a different type, the mock traps — throwing the
requirement's declared error type is a contract you control.
A typed-throws closure parameter (func run(_ body: () throws(MyError) -> Void))
is likewise stored untyped: its CallArgs/handler entry uses () throws -> Void,
so the mock never embeds a typed-throws function value.
A throwing @autoclosure argument is evaluated inside the same conversion, so its
error is re-thrown as the declared type too — the mock wraps its whole body rather
than only the handler call:
func compute(_ value: @autoclosure () throws -> Int) throws(ComputeError) -> Int
// generates:
// do {
// let value = try value()
// ...
// return try _handler(value)
// } catch {
// throw error as! ComputeError
// }As with an unrecorded call elsewhere, an autoclosure that throws does so before the
call is recorded, so CallCount is not incremented.
Two error types describe what an untyped clause already says, so the mock skips the re-throw for them.
throws(Never) declares a requirement that cannot throw. Its mock keeps the
signature but takes the non-throwing path — a non-throwing handler, called
without try:
func load(id: Int) throws(Never) -> String
// generates:
// var loadHandler: (@Sendable (Int) -> String)? = nil
// func load(id: Int) throws(Never) -> String { ... return _handler(id) }
let value = mock.load(id: 1) // no `try` neededThe same applies to a get throws(Never) property or subscript, and to an
@autoclosure () throws(Never) -> T parameter, which is evaluated without try.
Because the handler cannot throw, a throws(Never) requirement whose
@autoclosure parameter can throw is reported as a diagnostic rather than
mocked.
throws(any Error) is mocked exactly like untyped throws — a throwing handler
invoked with try, and no re-throw, since the handler's error already has the
declared type. The bare throws(Error) spelling is treated the same way.
Both are recognized by spelling, like the macro's other type checks: a generic
parameter or type alias named Never or Error is classified by its name.
Variadic parameters are tracked as arrays in CallArgs.
CallArgs stores the input snapshot before mutation.
For handlers:
- Single
inout, no return value:- handler returns the updated value
- Multiple
inout, no return value:- handler returns a tuple with updated values
inout+ return value:- handler returns
(returnValue: ..., inoutArgs: ...)
- handler returns
Example:
func removeFirst(_ array: inout [String]) -> StringExpected handler shape:
mock.removeFirstHandler = { array in
let first = array.first!
return (returnValue: first, inoutArgs: Array(array.dropFirst()))
}Read-only properties with get async, get throws, or get async throws are
mocked with a handler and a call counter instead of _name backing storage —
a stored value cannot model a thrown error, and the handler mirrors the
function model:
var token: String { get async throws }Generates:
var tokenCallCount: Int = 0
var tokenHandler: (@Sendable () async throws -> String)? = nil
var token: String {
get async throws { ... }
}Configure it in tests like a method handler:
mock.tokenHandler = { "secret" }
mock.tokenHandler = { throw AuthError.expired }If the handler is unset, the same defaults as methods apply: Optionals return
nil, arrays and sets return an empty collection, dictionaries return an empty
dictionary, and any other type calls fatalError.
Read-only subscripts with the same effects work identically:
subscript(key: String) -> Int { get async throws }
// generates:
// var subscriptStringHandler: (@Sendable (String) async throws -> Int)? = nil
// subscript(key: String) -> Int { get async throws { ... } }A protocol init requirement is satisfied by a generated required init witness that
mirrors the requirement's signature and records the call:
@Mockable
protocol Repository {
init(configuration: Configuration)
}Generates:
var initCallCount: Int = 0
var initCallArgs: [Configuration] = []
required init(configuration: Configuration) {
initCallCount += 1
initCallArgs.append(configuration)
}This unlocks protocols built around the init(configuration:) pattern — for example code
that constructs a conformer generically (Service(configuration:) where Service is a
generic constraint), which previously could not be mocked at all.
Notes:
- Initializers record only — there is no
initHandler. The recording state lives on the instance being created, so a per-instance handler could never be set before the initializer runs. InspectinitCallCount/initCallArgs(or the arguments you passed) to verify construction. async,throws, failability (init?), and generic clauses are preserved on the witness. A throwing initializer keeps itsthrowssignature but never throws.- The
requiredkeyword makes the initializer part of the mock's protocol conformance so that subclasses of the (non-final) mock inherit it. - When a protocol declares its own
initrequirements, the synthesized parameterlessinit()(normally generated forpublic/packagemocks) is omitted — therequired initwitnesses already provide accessible initializers. resetMock()clearsinitCallCountandinitCallArgsalongside the other tracking state.- For
Sendableandactormocks the recording goes throughMockableLocklike every other member, soinitCallCount/initCallArgsare lock-backed (andnonisolatedon actors). Anactorwitness omitsrequired, since actors arefinal.
A child mock inherits its parent mock's initializers, so a protocol whose parent declares
an init requirement is mockable through the inherited required init (and its recording).
Declaring a new init requirement directly on an inheriting protocol is not yet supported
— the witness would need to chain through the parent mock's initializer, which the macro cannot
see — and emits a diagnostic (see Diagnostics).
If a protocol inherits from Sendable (or uses @Sendable at protocol level), generated mocks:
- conform to
@unchecked Sendable - store mutable state behind
MockableLock
If a protocol inherits from Actor, generated mock type is an actor.
For test ergonomics, helper members are generated as nonisolated where possible, including:
- call counters
- call argument collections
- handlers
- backing properties (for setup)
resetMock()
Static methods/properties are always lock-backed through a shared static storage.
resetMock() also resets static generated members, clearing them in a single lock
acquisition so a concurrent caller never observes a partly reset mock.
A mock's tracking state is protected when the protocol says it should be: a Sendable
protocol, an Actor protocol, and a nonisolated requirement of a global-actor-isolated
protocol all select lock-backed storage, and static state is lock-backed regardless.
Everything else is plain stored properties, which is what a mock used from one task
needs.
Two things the macro cannot decide for you, because it only ever sees the protocol it is attached to:
- Inheritance chains. A mock is
Sendableif its own protocol is, and it subclasses its parent's mock whatever that parent chose. Mixing the two leaves half the state unprotected: aSendablechild of a non-Sendableparent inherits plain stored properties, and a non-Sendablechild of aSendableparent adds plain ones of its own while inheriting the parent's@unchecked Sendableconformance. DeclareSendableon every protocol in the chain, or none. - Global actors. Isolation is read from the protocol's own attributes, so a protocol that inherits its isolation from a parent produces an unisolated mock, and a protocol isolated to a custom global actor produces one too. Annotate each protocol you mock with the global actor it belongs to.
If a protocol inherits from another protocol and a parent mock exists:
- child mock inherits from
<Parent>Mock - child
resetMock()callssuper.resetMock()first - the child mock inherits the parent mock's initializers (it does not synthesize its own),
so a parent
initrequirement is satisfied through the inheritedrequired init
For multiple parent protocols, the first parent is used as the superclass target.
Protocol members inside #if / #elseif / #else are preserved in generated mocks.
resetMock() includes matching conditional branches so reset behavior stays aligned with active compilation conditions.
The mock declares the members of every branch, each under the branch's own condition, so requirements in sibling branches share one namespace: two same-name requirements in different branches of the same block are treated as overloads and get the usual disambiguating suffixes.
@Mockable
protocol Service {
#if CUSTOM
func fetch(id: Int) -> Int // fetchIntCallCount, fetchIntHandler, ...
#else
func fetch(name: String) -> Int // fetchStringCallCount, fetchStringHandler, ...
#endif
}By default the generated mock is wrapped in #if DEBUG. The condition:
argument selects a different compilation condition, or removes the guard:
@Mockable // #if DEBUG (default)
protocol UserService { ... }
@Mockable(condition: .custom("MOCKING")) // #if MOCKING
protocol PaymentService { ... }
@Mockable(condition: .always) // no #if guard
protocol PreviewDataService { ... }This unlocks setups where #if DEBUG is too narrow:
- a test-support module that is built in the release configuration,
- SwiftUI preview stubs that should not depend on the build configuration,
- a UI-test host app built for release.
Notes:
.custom("CONDITION")accepts any compilation condition expression, spelled as a string literal: a flag ("MOCKING"), or a compound condition built from identifiers,true/false,!,&&,||, parentheses, and platform checks such asos(...),arch(...),swift(...),compiler(...),canImport(...), andtargetEnvironment(...)— for example"DEBUG || UITESTS"or"os(iOS) && !RELEASE". Define each flag in every target that references the mock, viaSWIFT_ACTIVE_COMPILATION_CONDITIONS(Xcode) or.define("FLAG")inswiftSettings(SwiftPM)..alwaysemits the mock in every build configuration, including release. Use it deliberately — typically in a module that never ships.- The condition must be written literally at the attachment site. Macro
expansion happens at compile time, so runtime values (
condition: myFlag) and interpolated strings emit a diagnostic. - The condition only controls the
#ifwrapper around the mock; members of the protocol that sit inside their own#ifblocks keep those inner guards.
Compilation errors are emitted when:
@Mockableis applied to non-protocol declarations- unsupported members are present (for example a
static subscript) - a method or property requirement is named with an operator (
static func == (lhs: Self, rhs: Self) -> Bool) or with a backtick-escaped identifier (func `repeat`(),var `default`: Int { get }) - a new
initrequirement is declared directly on an inheriting protocol (not yet supported; inherited initializers still work) - a requirement's return type mentions a generic parameter inside a function type (for example
func makeSetter<T>() -> (T) -> Void) - a requirement takes a closure whose own parameters mention a generic parameter (for example
func observe<T>(_ handler: (T) -> Void)) - an argument other than
condition:is passed to@Mockable - the
condition:value is not written literally as.debug,.always, or.custom("CONDITION"), or the custom condition is not a valid compilation condition expression (identifiers,true/false,!,&&,||, parentheses, and platform checks)
- Static/class subscripts are not supported.
- Operator requirements and method or property names that need backtick escaping are not supported. Every generated member is named after the requirement (
fetchbecomesfetchCallCount,namebecomes_name), so a name that is not a plain identifier cannot produce legal identifiers. initrequirements are supported for standalone protocols (includingSendableandactormocks) and are inherited by child mocks. Declaring a newinitrequirement directly on an inheriting protocol is not yet supported.- Return-value methods and get-only subscript getters trigger
fatalErrorwhen the handler is unset, unless the return type has a natural empty value: Optionals returnnil, arrays and sets return an empty collection, and dictionaries return an empty dictionary.