SwiftMocking is a modern, type-safe mocking library for Swift that uses macros to provide a clean, readable, and efficient mocking experience. It offers an elegant API that leverages the power of parameter packs and @dynamicMemberLookup.
- Features
- Protocol Feature Support
- Installation
- Example
- Documentation
- Usage
- How it Works
- Generated Code Examples
- AI Agent Guide
- Known Limitations
| Feature | Description |
|---|---|
| Type-Safe Mocking | Uses parameter packs to keep mocks synchronized with protocol definitions, preventing runtime errors. |
| Clean, Readable API | Provides a Mockito-style API that makes tests expressive and easy to maintain. |
| Flexible Argument Matching | Offers powerful argument matchers like .any and .equal, with ExpressibleBy...Literal conformance for cleaner syntax. |
| Cross-Mock Call Order Verification | Verify that method calls occurred in a specific sequence, even across different mock objects with verifyInOrder(). |
| Effect-Safe Spies | Models effects like async and throws as phantom types, ensuring type safety when stubbing. |
| Compact Code Generation | Keeps the generated code as small and compact as possible. |
| Descriptive Error Reporting | Provides clear and informative error messages when assertions fail, making it easier to debug tests. |
| Options to configure the macro generated code | Exposes the MockableOptions OptionSet that enables selecting what and how code gets generated. |
| XCTest and Testing support | SwiftMocking uses swift-issue-reporting and exposes testing utilities to both XCTest and swift-testing frameworks. |
| Test Isolation for Concurrency | Provides isolation for concurrent test execution through TaskLocal. |
| Feature | Supported |
|---|---|
| Associated Types | ✅ |
| Variables | ✅ |
| Static Methods | ✅ |
| Generics | ✅ |
| Subscripts | ✅ |
async Methods |
✅ |
throws Methods |
✅ |
| Variadic parameters | ✅ |
| Closure parameters | ✅ |
| Metatype parameters | ✅ |
To add SwiftMocking to your Swift package, add it as a dependency in your Package.swift file:
.package(url: "https://github.com/DanielCardonaRojas/swift-mocking.git", from: "0.5.0"),Then, add SwiftMocking to your target's dependencies:
.target(
name: "MyTests",
dependencies: [
.product(name: "SwiftMocking", package: "swift-mocking"),
]
),For a comprehensive demonstration of SwiftMocking's capabilities, including various mocking scenarios and advanced features, please refer to the Examples project.
Here's an example of how to use Mockable to mock a PricingService protocol:
import SwiftMocking
@Mockable
protocol PricingService {
func price(_ item: String) throws -> Int
}Generated Code
class PricingServiceMock: Mock, PricingService {
func price(_ item: ArgMatcher<String>) -> Interaction<String, Throws, Int> {
Interaction(item, spy: super.price)
}
func price(_ item: String) throws -> Int {
return try adaptThrowing(super.price, item)
}
}Here is an example of a Store class that uses the PricingService.
class Store {
var items: [String] = []
var prices: [String: Int] = [:]
let pricingService: any PricingService
init<Service: PricingService>(pricingService: Service) {
self.pricingService = pricingService
}
func register(_ item: String) {
items.append(item)
let price = pricingService.price(for: item)
prices[item] = price
}
}In your tests, you can use the generated MockPricingService to create a mock object and stub its functions.
import SwiftMocking
import XCTest
final class StoreTests: XCTestCase {
func testItemRegistration() {
let mock = MockPricingService()
let store = Store(pricingService: mock)
// Stub specific calls
when(mock.price(for: "apple")).thenReturn(13)
when(mock.price(for: "banana")).thenReturn(17)
store.register("apple")
store.register("banana")
// Verify that price was called twice with any string
verify(mock.price(for: .any)).called(2) // .called(2) is equivalent to .called(.equal(2))
XCTAssertEqual(store.prices["apple"], 13)
XCTAssertEqual(store.prices["banana"], 17)
}
}For more detailed information, please refer to the official documentation.
For detailed examples of how @Mockable expands different protocol definitions into mock implementations, see Generated Code Examples.
Mockable provides a rich set of argument matchers to precisely control stubbing and verification.
// Stub a method to return a value regardless of the input string
when(mock.someMethod(.any)).thenReturn(10)
// Verify a method was called with any integer argument
verify(mock.anotherMethod(.any)).called()// Stub a method to return 10 only when called with "specific"
when(mock.someMethod(.equal("specific"))).thenReturn(10)
// Verify a method was called exactly with 42 (using literal conformance)
verify(mock.anotherMethod(42)).called()// Stub a method to return a value if the integer argument is less than 10
when(mock.processValue(.lessThan(10))).thenReturn("small")
// Verify a method was called with an integer argument greater than 100
verify(mock.processValue(.greaterThan(100))).called()// Using Swift's range syntax for more idiomatic matching
verify(mock.setVolume(.in(0...100))).called() // ClosedRange: 0 through 100
verify(mock.validateAge(.in(18...))).called() // PartialRangeFrom: 18 and above
verify(mock.setSpeed(.in(...65))).called() // PartialRangeThrough: up to 65
// Collection count matching with ranges
verify(mock.processBatch(.hasCount(in: 5...10))).called() // 5-10 items
verify(mock.handleLarge(.hasCount(in: 100...))).called() // 100+ items
verify(mock.processSmall(.hasCount(in: ...3))).called() // up to 3 items// Verify a specific method was never called
verifyNever(mock.sensitiveMethod(password: .any))
// Verify a mock object had no interactions at all
let unusedMock = MockPricingService()
verifyZeroInteractions(unusedMock) // Ensures mock was completely unusedAfter verifying that methods were called, you can inspect the actual arguments that were passed using the captured method:
verify(mock.calculate(a: .any, b: .any))
.captured { a, b in
print("Called calculate with: a=\(a), b=\(b)")
XCTAssertTrue(a + b > 0)
}class MyObject {}
let obj = MyObject()
// Stub a method to return a value only when called with the exact instance 'obj'
when(mock.handleObject(.identical(obj))).thenReturn("same instance")// Verify a method was called with a non-nil optional string
verify(mock.handleOptional(.notNil())).called()
// Stub a method to return a default value when called with a nil optional integer
when(mock.handleOptional(.nil())).thenReturn(0)enum MyError: Error { case invalid }
// Verify a method threw any error
verify(mock.performAction()).throws(.anyError())
// Verify a method threw an error of type MyError
verify(mock.processData()).throws(.error(MyError.self))Verify that method calls occurred in a specific order, even across different mock objects:
let pricingMock = MockPricingService()
let analyticsMock = MockAnalyticsService()
when(pricingMock.price("apple")).thenReturn(13)
_ = try pricingMock.price("apple")
analyticsMock.logEvent("purchase")
_ = try pricingMock.price("banana")
// Verify the sequence of calls across both mocks
verifyInOrder([
pricingMock.price("apple"),
analyticsMock.logEvent("purchase"),
pricingMock.price("banana")
])A powerful feature of SwiftMocking is that you can define the return value of a stub dynamically based on the arguments passed to the mocked function. This is achieved by providing a closure to thenReturn.
It is common in other testing frameworks, that the parameters of this closure be of type Any. However, thanks to the use of parameter packs, the set of arguments here are concrete types, and are guaranteed to match the types of the function signature that is being stubbed. This essentially enables substituting the mocked function dynamically. For example:
@Mockable
protocol Calculator {
func calculate(a: Int, b: Int) -> Int
}
// Calculate summing
when(mock.calculate(a: .any, b: .any)).thenReturn { a, b in
// Note that no casting is required, a and b are of type Int.
return a + b
}
XCTAssertEqual(mock.calculate(a: 5, b: 10), 15)
// Replace the calculation function
when(mock.calculate(a: .any, b: .any)).thenReturn(*)
XCTAssertEqual(mock.calculate(a: 5, b: 10), 50)Every when(...) call returns an arrangement object that can both stub return values and register side effects. Use .do { … } when you need to observe or mutate state without altering the stubbed response:
var events: [String] = []
when(mock.refresh(id: .equal("primary"))).do { id in
events.append("refresh called with \(id)")
}
when(mock.refresh(id: .equal("primary"))).then()
mock.refresh(id: "primary")
XCTAssertEqual(events, ["refresh called with primary"])For Void-returning interactions you can use the convenience alias then { … } instead of calling thenReturn(()). This keeps call sites concise while still allowing the same effect-specific APIs (throwing, async, async-throwing) shown above.
SwiftMocking provides a simple way to log method invocations on your mock objects. This can be useful for debugging tests and understanding the flow of interactions. You can enable logging on a per-instance or per-type basis.
To enable logging for a specific mock instance, set the isLoggingEnabled property to true.
let mock = MockPricingService()
mock.isLoggingEnabled = true
// Any calls to mock.instance methods will now be logged to the console.
_ = mock.price(for: "apple")
// Output: PricingServiceMock.price("apple")SwiftMocking excels at testing methods that use completion handlers or callbacks. This is particularly useful for testing asynchronous operations like network requests, file I/O, or any method that takes a closure parameter.
When testing callbacks, use the .any matcher for the callback parameter and the .then closure to control how the callback is executed:
@Mockable
protocol NetworkService {
func fetchUser(id: String, completion: @escaping (Result<User, Error>) -> Void)
}
func testNetworkServiceCallback() async {
let mock = MockNetworkService()
let expectation = XCTestExpectation()
// Use .any matcher for the callback parameter
when(mock.fetchUser(id: .equal("123"), completion: .any)).then { id, completion in
// Control when and how the callback is executed
completion(.success(User(id: id, name: "Test User")))
}
mock.fetchUser(id: "123") { result in
switch result {
case .success(let user):
XCTAssertEqual(user.name, "Test User")
expectation.fulfill()
case .failure:
XCTFail("Expected success")
}
}
await fulfillment(of: [expectation], timeout: 1.0)
}Important: When testing methods with callbacks, always use the .any matcher for callback parameters, as it's the only matcher that makes sense for closure types.
When a system under test triggers a dependency inside a detached task, you can wait for the interaction with the until helper.
struct Controller {
let refresh: (String) async throws -> Void
func start() {
Task {
try await Task.sleep(for: .milliseconds(25))
try await refresh("primary")
}
}
}
func testControllerRefreshesInBackground() async throws {
let spy = Spy<String, AsyncThrows, Void>()
let sut = Controller(refresh: adapt(spy))
sut.start()
try await until(spy("primary"))
verify(spy("primary")).called()
}SwiftMocking also supports testing systems that use closures as dependencies instead of protocols. This is particularly useful for projects using The Composable Architecture (TCA) from Point-Free or similar dependency injection approaches.
// Define a struct with closure-based dependencies
struct FetchClient {
var loadNumber: () async throws -> [Int]
var saveNumber: (Int) async throws -> Void
}
func testClosureBasedDependencies() async throws {
// Create spies for each closure
let loadNumberSpy = Spy<Void, AsyncThrows, [Int]>()
let saveNumberSpy = Spy<Int, AsyncThrows, Void>()
// Stub the behaviors
when(loadNumberSpy(.any)).thenReturn([1, 2, 3])
when(saveNumberSpy(.any)).then { number in
print("Saving number: \(number)")
}
// Create the client with adapted spies
let client = FetchClient(
loadNumber: adapt(loadNumberSpy),
saveNumber: adapt(saveNumberSpy)
)
// Use the client
let numbers = try await client.loadNumber()
try await client.saveNumber(42)
// Verify interactions
XCTAssertEqual(numbers, [1, 2, 3])
verify(loadNumberSpy(.any)).called(1)
verify(saveNumberSpy(42)).called(1)
}This approach provides the same testing capabilities as protocol-based mocking but works with closure-based dependency injection patterns. The adapt() function converts a Spy into a closure that can be used directly as a dependency.
SwiftMocking provides test isolation to ensure concurrent tests don't interfere with each other when using static mocks. This is essential for Swift Testing which runs tests in parallel by default.
- XCTest: Inherit from
MockingTestCaseinstead ofXCTestCasefor automatic spy isolation - Swift Testing: Use the
@Test(.mocking)trait to enable test scoping
Without proper isolation, concurrent tests can experience race conditions where static spies accumulate calls from multiple tests, making verification assertions unpredictable.
SwiftMocking provides a powerful trait-based system for injecting custom default values that are scoped to individual tests or test suites. This allows you to provide specific default return values for unstubbed mock methods within the scope of a test execution.
import Testing
import SwiftMocking
@Test(.withDefaults("Test User", 42, true))
func testWithCustomDefaults() {
let mock = MockUserService()
// Unstubbed methods return the custom defaults
let name = mock.getUserName() // Returns "Test User"
let age = mock.getUserAge() // Returns 42
let isActive = mock.isUserActive() // Returns true
#expect(name == "Test User")
#expect(age == 42)
#expect(isActive == true)
}Apply default values to an entire test suite:
@Suite(.withDefaults("Default User"))
struct UserServiceTests {
@Test
func testUserCreation() {
let mock = MockUserService()
let name = mock.getUserName() // Returns "Default User"
}
@Test(.withDefaults("Override User"))
func testWithOverride() {
let mock = MockUserService()
let name = mock.getUserName() // Returns "Override User"
}
}- Test Isolation: Each test gets its own isolated default value scope
- Concrete Values: Use actual instances instead of static default implementations
- Type Safety: Compile-time validation ensures type correctness
- Flexible: Different tests can have different defaults for the same types
- Composable: Works seamlessly with other traits like
.mocking
SwiftMocking provides a mechanism to return default values for methods that have not been explicitly stubbed. This is achieved through the DefaultProvidable protocol and the DefaultProvidableRegistry.
DefaultProvidableProtocol: Types conforming to this protocol can provide adefaultValue.DefaultProvidableRegistry: This registry manages and provides access to default values for registeredDefaultProvidabletypes.
Without a mechanism to provide default/fallback values when a method is not stubbed, calling the mock would unavoidably result in a fatalError.
For this reason, and to providide a less rigid testing experience, generated mocks include a defaultProviderRegistry property. This provides the flexibility of not having to stub every combination of arguments of a function, for certain return types.
By default, common Swift types like String, Int, Double, Float, Bool, Optional, Array, Dictionary, and Set conform to DefaultProvidable and are automatically registered.
// Assuming MyServiceMock is generated by @Mockable macro
let mock = MyServiceMock()
// If 'fetchData' is not stubbed, and its return type (e.g., String) is DefaultProvidable,
// it will return the default value for String ("")
let data = mock.fetchData() // data will be ""You can also register your custom types that conform to DefaultProvidable:
struct MyCustomType: DefaultProvidable {
static var defaultValue: MyCustomType {
return MyCustomType(name: "Default", value: 0)
}
let name: String
let value: Int
}
// Register your custom type with the shared registry
DefaultProvidableRegistry.shared.register(MyCustomType.self)
// Now, if a method returns MyCustomType and is unstubbed, it will return MyCustomType.defaultValue
let customValue = mock.getCustomType() // customValue will be MyCustomType(name: "Default", value: 0)Mockable provides detailed error messages when a test assertion fails. For example, if you expect a function to be called 4 times but it was only called twice, you'll get a clear message indicating the discrepancy.
// Example of a failing test
verify(mock.price(for: .any)).called(4)This will produce the following error:
error: Unfulfilled call count. Actual: 2
SwiftMocking includes a comprehensive guide for AI coding assistants to help generate high-quality unit tests automatically. This guide enables AI tools like Claude Code, GitHub Copilot, and ChatGPT to understand SwiftMocking patterns and create consistent, well-structured tests.
The AGENT_GUIDE.md contains everything AI tools need to know about SwiftMocking, including:
- Framework fundamentals and setup patterns
- Stubbing and verification strategies
- Argument matching techniques
- Common testing scenarios and best practices
- Integration with Swift Testing and XCTest
Copy this URL to provide the complete guide to your preferred AI coding assistant:
https://raw.githubusercontent.com/DanielCardonaRojas/swift-mocking/main/AGENT_GUIDE.md
Example Prompt:
Please fetch and review this SwiftMocking guide: https://raw.githubusercontent.com/DanielCardonaRojas/swift-mocking/main/AGENT_GUIDE.md
Then help me write comprehensive unit tests for my [YourService] protocol following the patterns and best practices in the guide.
SwiftMocking leverages the power of Swift macros to generate mock implementations of your protocols. When you apply the @Mockable macro to a protocol, it generates a new class that inherits from a Mock base class. This generated mock class conforms to the original protocol.
The Mock base class uses @dynamicMemberLookup to create and manage spies to for every protocol requirement.
A Spy has this structure:
let spy = Spy<ParamType1, ParamType2, ParamTypeN, Effect, ReturnType>()
// So for example:
// Represents a function signature: (Bool, Int) async throws -> String?
let methodSpy = Spy<Bool, Int, AsyncThrows, Optional<String>>()The use of parameter packs here allows creating any number of parmeter types ParamType1 ... ParamTypeN.
This approach eliminates the need for manual mock implementations and provides a clean, expressive, and type-safe API for your tests.
Currently, Xcode's autocomplete feature may not work as expected when using the generated mock objects. This seems to be a known issue with Xcode. This limitation could be worked around by conforming to the mocked protocol within an extension. However due to limitations of Swift macros, generating this extension will result in an error.
For example, the ideal generated code would separate the protocol conformance into an extension, like this:
// Ideal generated code
public protocol PricingService {
func price(_ item: String) throws -> Int
}
class PricingServiceMock: Mock {
func price(_ item: ArgMatcher<String>) -> Interaction<String, Throws, Int> {
Interaction(item, spy: super.price)
}
}
extension PricingServiceMock: PricingService {
func price(_ item: String) throws -> Int {
return try adaptThrowing(super.price, item)
}
}Xcode's autocomplete will prioritize methods in the order they are declared. Since mocks are usualy not interacted with directly we opt for declaring the Interaction methods first.
This project is licensed under the MIT License - see the LICENSE file for details.