SwiftMocking
Mocking for Swift protocols: @Mockable generates mock classes; when(...) stubs, verify(...) asserts calls. When the macro can't generate a mock — most notably protocol inheritance (protocol B: A — the macro drops inherited requirements and the mock fails to conform) — hand-write the mock following the exact generated-code shape.
The macro is not the only entry point. Two escape hatches cover everything it can't reach, and both are fully supported (see spies-and-composition.md):
Spyused directly — a standalone recorder needing noMocksubclass and no macro. This is how you mock classes, which@Mockablecannot touch at all: subclass the class and back each override with aSpy.- Composition over inheritance — hold a
let mock = Mock()property instead of inheriting fromMock.Mock's@dynamicMemberLookupsubscript ispublic, somock.nameresolves spies from outside the class. Required when the type already has a superclass (UIViewController, a legacy base class) or isn't a class at all (struct, actor). Substitutesuper.name→self.mock.nameandadapt(...)→Mock.adapt(...); everything else in manual-mocking.md is unchanged.
The macro generates the composed form itself with @Mockable([.composition]) — no hand-writing — when the blocker is a protocol whose conformers must inherit a class:
@Mockable([.composition])
protocol ViewControllerService: SampleBase { ... }
Hand-write composition only for what the macro still can't express: structs, actors, and mocking a concrete class.
When to use what
| Situation | Read |
|---|---|
| Protocol has no inheritance; need a mock | @Mockable protocol P {...} → use PMock() |
| Protocol inherits another protocol with members | manual-mocking.md (macro cannot do this) |
Protocol constrained to a class (protocol P: SomeClass) |
@Mockable([.composition]) — the macro handles it |
| Mocking a class (no protocol available) | spies-and-composition.md — subclass + raw Spy |
Can't inherit Mock (struct, actor, or a hand-written type with a superclass) |
spies-and-composition.md — let mock = Mock() |
| Mocking without any protocol (closure/TCA dependencies) | usage.md / spies-and-composition.md — Spy + adapt |
Stubbing/verifying settable properties or subscripts ({ get set }) |
usage.md — Properties & subscripts; hand-written shape in manual-mocking.md |
| Need mock source without the macro (plugin unavailable, codegen, review) | mockable CLI (below) when available; hand-writing per manual-mocking.md is always valid |
@Sendable/Swift 6 concurrency errors when stubbing |
sendable.md |
| Stubbing / matching / verifying API reference | usage.md |
| Exact signature of a public API (overloads, constraints, defaults) | references/interface/ (below) |
API interface
Machine-generated from the compiled modules — the authoritative signature reference. Check these before guessing at an overload or a generic constraint:
- Core library:
references/interface/SwiftMocking.swiftinterface - XCTest/swift-testing helpers:
references/interface/SwiftMockingTestSupport.swiftinterface
One caveat: Sendable is a marker protocol and the compiler elides it from
where clauses in emitted interfaces — thenThrow<E: Error & Sendable> prints
as where E : Error. The constraint is still enforced. For Sendable questions
trust sendable.md, not these files.
Deterministic generation: the mockable CLI
When a mock must exist as written source (macro plugin unavailable, codegen pipeline, review), the mockable CLI is the fastest exact path. Hand-writing per manual-mocking.md is equally correct and needs nothing; use the CLI when it's available, hand-write when it isn't or when you're already customizing.
echo 'protocol P { func price(_ item: String) throws -> Int }' | mockable
Invoke as mockable when it is on PATH; otherwise .build/release/mockable inside a swift-mocking checkout (build once with swift build -c release --product mockable, then optionally copy the binary onto PATH).
- Input needs no annotation — paste the protocol as it is written in the source you're mocking.
- Pass options with
--options, comma-separated (--options composition,suffixMock). Use it for protocols you can't or shouldn't annotate — most importantly--options compositionfor a protocol constrained to a class, the fastest way to see the composed shape:echo 'protocol P: UIViewController { func load() }' | mockable --options composition - Options may still ride the input as
@Mockable([.suffixMock]) protocol P {...}, which wins over--options. Prefer the flag: it keeps the stdin text identical to the real declaration. - Default output keeps the macro's
#if DEBUGwrapper; pass--no-debug-wrapwhen pasting into a test target (DEBUG is per build configuration — a wrapped mock vanishes underswift test -c release). - A stderr warning about inherited requirements means the output will not conform — hand-write per manual-mocking.md instead.
- Output keeps the macro's zero-arg/property-getter shape:
when(...)silently stubs a disconnected spy, andverify(...)reports zero calls for those members — apply the pinned-spy fix from manual-mocking.md before relying on them.
The one rule for manual mocks
Every protocol requirement gets two members in the mock class:
- Runtime member — fulfills the protocol, forwards to the spy:
adapt(super.method, args) - Interaction member — same name,
ArgMatcher<T>parameters, returnsInteraction<Inputs..., Effect, Output>— whatwhen(...)/verify(...)consume
class FooMock: Mock, @unchecked Sendable, Foo {
func price(_ item: String) throws -> Int {
return try adaptThrowing(super.price, item)
}
func price(_ item: ArgMatcher<String>) -> Interaction<String, Throws, Int> {
Interaction(item, spy: super.price)
}
}
Class shell: inherit Mock first, restate @unchecked Sendable, conform to the most-derived protocol only, match access levels. If inheriting Mock isn't possible, keep both members and compose instead — let mock = Mock(), super.price → mock.price, adaptThrowing(...) → Mock.adaptThrowing(...) (spies-and-composition.md).
Full recipe (inheritance flattening, properties, subscripts, variadics, generics, statics, initializers): manual-mocking.md. Zero-parameter methods and property getters need the pinned-spy pattern described there — the macro-generated form silently mis-stubs them.
Quick verification checklist
A correct mock (manual or generated) round-trips:
let mock = FooMock()
when(mock.price(.any)).thenReturn(42)
let svc: Foo = mock // protocol-typed: avoids overload traps
_ = try svc.price("apple")
verify(mock.price(.equal("apple"))).called(1)
Known sharp edges
@Mockableonprotocol B: A→ compile errordoes not conform to protocol 'A'(inherited requirements never generated). Hand-write per manual-mocking.md.@Mockableonprotocol P: SomeClass→requires that 'MockP' inherit from 'SomeClass'. This is a class constraint, not protocol inheritance, and no hand-edit fixes the default output — the mock needs its superclass slot forSomeClass, and the default strategy needs it forMock. Add[.composition]. The option is opt-in: the macro can't tell a class from a protocol by name, so it never infers it.@Mockableonly accepts protocols — never a class. To fake a class, subclass it and back overrides with rawSpyproperties (spies-and-composition.md).finalclasses/members can't be faked either way; extract a protocol.- In a composed mock, plain
adapt(...)doesn't resolve — it's an instance method onMockand you didn't inherit it. Use the staticMock.adapt(...)/Mock.adaptThrowing(...).clear()does resolve:MockProvidingsupplies both the instance and static forms. - Hand-written composed mocks must spell
self.mock.name, not a baremock.name, wherever the spy is read inside a closure — settable members do exactly that, and Swift rejects the bare form withrequires explicit use of 'self' to make capture semantics explicit.supernever needed the qualifier.[.composition]output already does this. - Zero-arg members (
func start(), property getters):when(mock.getX()).thenReturn(v)does not reach the runtime member in macro-generated mocks. Manual mocks fix this with the pinned-spy pattern. - Stub API is
thenReturn/thenThrow/do— there is no.then. - Never write
thenReturn(()).Voidis in the default-value registry, so an unstubbed-> Voidmember just runs and records the call — stub it only to throw or to attach adoside effect. The registry also coversBool/String/Int/Double/Float/Array/Set/Dictionary/Optional, so those members are callable unstubbed too (usage.md — Default values). - Spy names come from the requirement, ignoring argument labels: methods use their name, subscripts are namespaced as
subscript+ParameterNames (subscript(row:column:)→subscriptRowColumn), settable members addset+Name. The prefix keeps a subscript from colliding with a method or variable of the same name. The compiler already rejects most same-key cases (two subscripts differing only by argument label, or avar xbeside afunc x(), are both invalid redeclarations); the one that compiles but mocks incorrectly is two methods differing only by argument label (fetch(id:)/fetch(name:)), which silently share a spy — rename one or vary the parameter types. Same-name/different-signature overloads are fine. - Settable members (
{ get set }) record reads and writes on separate spies:verify(mock.x)counts reads,verify(mock.x <- v)counts writes. A write never registers as a read. - Bare
mock.start()(zero-arg) is ambiguous on the mock type — call via a protocol-typed reference. - Literal arguments on the mock type dispatch to the interaction member (
mock.fetchUser(id: "1")returns anInteractioninstead of calling through) — call the mock via a protocol-typed reference.
References
manual-mocking.md— hand-writing mock classes; inheritance chains; pinned-spy patternspies-and-composition.md— usingSpydirectly; mocking classes; composinglet mock = Mock()for structs/actors/existing superclassesusage.md— when/verify/matchers/stubbing referencesendable.md— Swift 6 concurrency contract and non-Sendable workarounds