The Swift testing ecosystem has long been dominated by traditional mocking frameworks that rely on runtime magic and type erasure. SwiftMocking takes a fundamentally different approach, leveraging Swift's most advanced language features to create something genuinely novel—a mocking library where type safety isn't an afterthought, but the foundation.
The Architecture Behind the Magic
What makes SwiftMocking unique isn't just its API, but its architectural foundation. Built on Swift 5.9's parameter packs, phantom types, and macros, it represents a complete rethinking of how test doubles should work in a type-safe language.
Parameter Packs: The Game Changer
At the heart of SwiftMocking lies an elegant use of parameter packs that enables something previously impossible—perfect type preservation across any function signature (aka arity):
class Spy<each Input, Effects: Effect, Output> { // Can represent ANY function signature while maintaining complete type safety } // These spies capture the exact shape of their target functions: let simpleSpy = Spy<String, None, Int>() // (String) -> Int let asyncSpy = Spy<String, Async, Data>() // (String) async -> Data let throwingSpy = Spy<String, Int, Throws, Bool>() // (String, Int) throws -> Bool let asyncThrowsSpy = Spy<String, Int, Bool, AsyncThrows, [User]>() // (String, Int, Bool) async throws -> [User] This approach eliminates the fundamental compromise that has plagued mocking frameworks: the choice between type safety and expressiveness. With SwiftMocking, you get both.
Effects as Phantom Types
Another characteristic of SwiftMocking's is how it models Swift's effects (async, throws). It does this by threading a phantom type through all the library types to ensure that only the correct tests can be written.
// The effect types thread through all library APIs let spy = Spy<String, AsyncThrows, Data>() when(spy(.any)).thenReturn { input in // Compiler enforces that this closure must be async throws await someAsyncOperation(input) } // This verification is also effect-aware try await until(spy("test")) // Can only be called on async spies The Self-Managing Mock Infrastructure
The Mock base class represents another architectural innovation. Using @dynamicMemberLookup, it automatically creates and manages spy instances on demand:
@dynamicMemberLookup open class Mock: DefaultProvider { subscript<each Input, Eff: Effect, Output>( dynamicMember member: String ) -> Spy<repeat each Input, Eff, Output> { // Lazily creates typed spies for protocol requirements } } This design keeps generated code minimal while providing unlimited flexibility. Each mock is essentially a spy factory that creates exactly the right spy type for each method signature.
Macro-Free Testing
While the @Mockable macro provides convenience, the core library is designed to work without it. This enables powerful closure-based testing patterns, for example:
// Test systems that use dependency injection with closures struct APIClient { let fetchUser: (String) async throws -> User let updateUser: (User) async throws -> User } func testAPIFlow() async throws { let fetchSpy = Spy<String, AsyncThrows, User>() let updateSpy = Spy<User, AsyncThrows, User>() when(fetchSpy(.any)).thenReturn(User(id: "123", name: "Test")) when(updateSpy(.any)).thenReturn { user in // Dynamic behavior with perfect type safety User(id: user.id, name: user.name.uppercased()) } let client = APIClient( fetchUser: adapt(fetchSpy), // Convert spy to closure updateUser: adapt(updateSpy) ) let result = try await client.updateUser(User(id: "456", name: "test")) verify(updateSpy(.any)).called() } Dynamic Stubbing with Type Safety
Traditional mocking frameworks force you to work with Any parameters in dynamic stubs. SwiftMocking preserves exact types:
@Mockable protocol Calculator { func calculate(a: Int, b: Int) -> Int } when(calculator.compute(a: .even(), b: .even())).thenReturn { a, b in a + b } when(calculator.compute(a: .odd(), b: .odd())).thenReturn { a, b in a * b } Sophisticated Argument Matching
The argument matching system leverages Swift's literal protocols and range syntax for natural expressions:
// Swift ranges work naturally verify(mock.setVolume(.in(0...100))).called() verify(mock.processItems(.hasCount(in: 5...))).called() // Literal values work without explicit matchers verify(mock.authenticate("user@example.com")).called() verify(mock.setFlag(true)).called(.greaterThan(2)) verify(mock.calculatePrice(199.99)).called() Compact Code Generation
The macro generates remarkably clean code. For a simple protocol:
@Mockable protocol PricingService { func price(for item: String) throws -> Int } The entire generated mock is just:
#if DEBUG class MockPricingService: Mock, PricingService { func price(for item: ArgMatcher<String>) -> Interaction<String, Throws, Int> { Interaction(item, spy: super.price) } func price(for item: String) throws -> Int { return try adaptThrowing(super.price, item) } } #endif Two methods: one for the fluent testing API, one for protocol conformance. No bloat, no generated noise.
Practical Benefits
Test Isolation by Design
SwiftMocking leverages Swift's TaskLocal values to enable testing protocols with static requirements which would be impossible without this feature.
In a similar manner the library provides TestScoping fallback values for unstubbed methods through suite and test traits, when using the Testing framework.
Framework Integration
The library works seamlessly with both XCTest and Swift Testing, all though it may be more convenient in when using the Testing framework:
// Swift Testing with automatic test isolation @Test(.mocking) func testConcurrentExecution() { // Each test gets isolated spy state } // XCTest with isolation through inheritance class MyTests: MockingTestCase { // Automatic spy isolation per test method } Fallback Value System
A sophisticated default value system prevents fatalError for unstubbed methods:
// Built-in defaults for common types let mock = MockUserService() let name = mock.getUserName() // Returns "" let age = mock.getUserAge() // Returns 0 // Custom types can participate struct User: DefaultProvidable { static var defaultValue: User { User(id: "", name: "") } } SwiftMocking is available on GitHub with comprehensive documentation, real-world examples, and integration guides for both XCTest and Swift Testing.
Top comments (0)