A Swift mocking library inspired by Kotlin's mockk, providing elegant mocking capabilities for protocol-based testing.
- Test-target only dependency - No need to add SwiftMockk to your main target
- Build-phase mock generation - Automatic mock class generation using SPM build tool plugin
- Intuitive DSL -
every { }andverify { }syntax inspired by mockk - Argument matching - Flexible matchers including
any(),eq(), and custom predicates - Async/await support - Full support for Swift's async/await patterns
- Type-safe - Compile-time type checking for all mock interactions
- Verification modes -
exactly,atLeast,atMostcall count verification - Property mocking - Full support for both get and get/set properties
- Order verification - Verify calls happened in a specific order with
verifyOrder()andverifySequence() - Relaxed mocks - Optional mode that returns default values for unstubbed methods
- Result type support - Convenience methods for stubbing
Result<Success, Failure>return types - Typed throws support - Full support for Swift 6's typed throws syntax
throws(ErrorType) - Generics support - Full support for generic methods, generic protocols, and associated types
- Kotlin-style mockk() function - Create mocks using
mockk(Protocol.self)syntax
- Swift 6.0+
- macOS 12+ / iOS 13+
Add SwiftMockk to your Package.swift:
dependencies: [
.package(url: "https://github.com/TheRogue76/SwiftMockk", from: "<version>")
],
targets: [
.target(
name: "YourApp",
dependencies: [] // No SwiftMockk dependency needed in main target!
),
.testTarget(
name: "YourAppTests",
dependencies: ["YourApp", "SwiftMockk"],
plugins: [.plugin(name: "SwiftMockkGeneratorPlugin", package: "SwiftMockk")]
)
]If using via Xcode:
- In Xcode, select “File” → “Add Packages...”
- Enter https://github.com/TheRogue76/SwiftMockk.git
- Make sure to ONLY add the package to your test target, not the main target
- Under the Test target's build settings -> Run build tools plugin, add the
SwiftMockkGeneratorPluginlike so:
In your main target, mark protocols with the // swiftmockk:generate comment. No imports needed!
// UserService.swift (in your main target)
// swiftmockk:generate
protocol UserService {
func fetchUser(id: String) async throws -> User
func deleteUser(id: String) async throws
func updateUser(_ user: User) async throws -> User
}The build tool plugin automatically scans for marked protocols and generates a MockUserService class during compilation.
You can create mocks in two ways:
import Testing
@testable import YourModule
import SwiftMockk
@Test func testWithMockk() async throws {
// Create mock using mockk() - similar to Kotlin
let mock = mockk(UserService.self)
// Create relaxed mock
let relaxedMock = mockk(UserService.self, mode: .relaxed)
// Use as normal
await every { try await mock.fetchUser(id: "123") }.returns(expectedUser)
}Note: The first time you instantiate any mock (using mockk() or direct instantiation), all mocks in that file are automatically registered with the registry.
let mock = MockUserService()
let relaxedMock = MockUserService(mode: .relaxed)Both approaches work identically. The mockk() function provides a more Kotlin-like API.
Error Handling: If you need to handle registration errors, use tryMockk():
do {
let mock = try tryMockk(UserService.self)
} catch MockRegistryError.notRegistered(let name) {
// Handle missing mock registration
}Limitation: Generic protocols (those with associated types) cannot use mockk(). Use direct instantiation instead:
// For generic protocols, use direct instantiation:
let repo = MockRepository<User>() // Works
// mockk(Repository<User>.self) // Won't workDue to Swift's lazy evaluation of file-level constants, mockk() requires special handling when used as a stored property initializer (before any mock has been instantiated):
// ❌ This will crash - mockk() called before any mock is instantiated
final class MyTests: XCTestCase {
let mock: UserService = mockk(UserService.self) // Crashes!
}Solutions:
Option 1: Call _swiftMockkBootstrap() in class setUp (Recommended for XCTest)
final class MyTests: XCTestCase {
var mock: UserService! // Change to var
override class func setUp() {
super.setUp()
_swiftMockkBootstrap() // Triggers mock registration
}
override func setUp() {
super.setUp()
mock = mockk(UserService.self) // Now works!
}
}Option 2: Use direct instantiation
final class MyTests: XCTestCase {
let mock: UserService = MockUserService() // Always works
}Option 3: Use lazy var
final class MyTests: XCTestCase {
lazy var mock: UserService = mockk(UserService.self)
override func setUp() {
super.setUp()
_ = mock // Accessing lazy var triggers registration
}
}Why this happens: Swift evaluates file-level constants lazily. The mock registration code only runs when a mock is first instantiated. When mockk() is called as a stored property initializer, no mock has been instantiated yet, so the registry is empty.
When mockk() works without workarounds:
- When called inside test methods (after setUp has run)
- When called after any mock has been directly instantiated
- In Swift Testing
@Testfunctions (they run after module initialization)
import Testing
@testable import YourModule
import SwiftMockk
@Test func testBasicStubbing() async throws {
let mock = mockk(UserService.self)
let expectedUser = User(id: "123", name: "Alice")
// Stub the method
await every { try await mock.fetchUser(id: "123") }.returns(expectedUser)
// Call the mock
let user = try await mock.fetchUser(id: "123")
// Verify the result
#expect(user == expectedUser)
#expect(user.name == "Alice")
}@Test func testBasicVerification() async throws {
let mock = MockUserService()
// Stub
await every { try await mock.fetchUser(id: "123") }.returns(User(id: "123", name: "Alice"))
// Call the method
_ = try await mock.fetchUser(id: "123")
// Verify it was called
await verify { try await mock.fetchUser(id: "123") }
}SwiftMockk provides flexible argument matching:
@Test func testAnyMatcher() async throws {
let mock = MockUserService()
let defaultUser = User(id: "0", name: "Default")
// Stub with any() matcher
await every { try await mock.fetchUser(id: any()) }.returns(defaultUser)
// Call with different IDs
let user1 = try await mock.fetchUser(id: "123")
let user2 = try await mock.fetchUser(id: "456")
// All return the default user
#expect(user1 == defaultUser)
#expect(user2 == defaultUser)
}@Test func testCustomMatcher() async throws {
let mock = MockUserService()
let longIdUser = User(id: "long", name: "Long ID User")
// Stub with custom matcher - only match IDs longer than 5 characters
await every {
try await mock.fetchUser(id: match { $0.count > 5 })
}.returns(longIdUser)
// Call with long ID
let result = try await mock.fetchUser(id: "verylongid")
#expect(result == longIdUser)
}@Test func testVerificationExactly() async throws {
let mock = MockUserService()
await every { try await mock.fetchUser(id: any()) }.returns(User(id: "0", name: "Default"))
// Call exactly twice
_ = try await mock.fetchUser(id: "123")
_ = try await mock.fetchUser(id: "456")
// Verify exactly 2
await verify(times: .exactly(2)) { try await mock.fetchUser(id: any()) }
}
@Test func testVerificationAtLeast() async throws {
let mock = MockUserService()
await every { try await mock.fetchUser(id: any()) }.returns(User(id: "0", name: "Default"))
// Call twice
_ = try await mock.fetchUser(id: "123")
_ = try await mock.fetchUser(id: "456")
// Verify at least once
await verify(times: .atLeast(1)) { try await mock.fetchUser(id: any()) }
// Verify at least twice
await verify(times: .atLeast(2)) { try await mock.fetchUser(id: any()) }
}@Test func testBasicStubbing() async throws {
let mock = MockUserService()
let expectedUser = User(id: "123", name: "Alice")
// Stub the method
await every { try await mock.fetchUser(id: "123") }.returns(expectedUser)
// Call the mock
let user = try await mock.fetchUser(id: "123")
#expect(user == expectedUser)
}@Test func testThrowingMethod() async throws {
let mock = MockUserService()
// Stub to throw an error
await every { try await mock.deleteUser(id: any()) }.throws(ServiceError.notFound)
// Verify it throws
do {
try await mock.deleteUser(id: "123")
Issue.record("Expected method to throw")
} catch {
// Expected
#expect(error is ServiceError)
}
}| Feature | Kotlin mockk | SwiftMockk |
|---|---|---|
| Mock creation | mockk<Service>() |
mockk(Service.self) or MockService() |
| Stubbing | every { mock.method() } returns value |
await every { await mock.method() }.returns(value) |
| Verification | verify { mock.method() } |
await verify { await mock.method() } |
| Async stubbing | coEvery { mock.method() } returns value |
Same as stubbing (unified API) |
| Matchers | any(), eq(), match {} |
any(), eq(), match {} |
| Call count | verify(exactly = 2) { } |
verify(times: .exactly(2)) { } |
| Relaxed mocks | mockk(relaxed = true) |
mockk(Service.self, mode: .relaxed) |
Due to Swift's language design and concurrency model, SwiftMockk has some differences:
- Async by default: All DSL functions (
every,verify) are async in SwiftMockk because Swift's actor-based concurrency requires async access - Build-phase generation: Swift uses build-phase code generation instead of runtime bytecode manipulation
- Protocol-only: Can only mock protocols, not classes (Swift limitation)
- Explicit await: Swift requires explicit
awaitkeywords for async operations
// swiftmockk:generate
protocol ServiceWithProperties {
var name: String { get set }
var count: Int { get }
}
@Test func testPropertyStubbing() async throws {
let mock = MockServiceWithProperties()
// Stub property getter
await every { mock.name }.returns("TestName")
// Get property
let name = mock.name
// Verify
#expect(name == "TestName")
await verify { mock.name }
}
@Test func testPropertySetter() async throws {
let mock = MockServiceWithProperties()
// Set property
mock.name = "NewName"
// Verify setter was called
await verify { mock.name = "NewName" }
}
@Test func testReadOnlyProperty() async throws {
let mock = MockServiceWithProperties()
// Stub read-only property
await every { mock.count }.returns(42)
// Get property
let count = mock.count
// Verify
#expect(count == 42)
}@Test func testVerifyOrder() async throws {
let mock = MockUserService()
// Stub
await every { try await mock.fetchUser(id: any()) }.returns(User(id: "0", name: "Default"))
await every { try await mock.deleteUser(id: any()) }.returns(())
// Call in order: fetch, delete, fetch
_ = try await mock.fetchUser(id: "1")
try await mock.deleteUser(id: "1")
_ = try await mock.fetchUser(id: "2")
// Verify order (non-consecutive)
await verifyOrder {
try await mock.fetchUser(id: any())
try await mock.deleteUser(id: any())
}
}
@Test func testVerifySequence() async throws {
let mock = MockUserService()
// Stub
await every { try await mock.fetchUser(id: any()) }.returns(User(id: "0", name: "Default"))
await every { try await mock.deleteUser(id: any()) }.returns(())
// Call in sequence: fetch, delete
_ = try await mock.fetchUser(id: "1")
try await mock.deleteUser(id: "1")
// Verify exact consecutive sequence
await verifySequence {
try await mock.fetchUser(id: "1")
try await mock.deleteUser(id: "1")
}
}// swiftmockk:generate
protocol CalculatorService {
func add(a: Int, b: Int) -> Int
func getName() -> String
func isReady() -> Bool
}
@Test func testRelaxedMockReturnsDefaults() async throws {
let mock = MockCalculatorService(mode: .relaxed)
// Call without stubbing - should return default values for primitives
let result = mock.add(a: 5, b: 10)
let name = mock.getName()
let ready = mock.isReady()
// Should return default values
#expect(result == 0) // Default Int
#expect(name == "") // Default String
#expect(ready == false) // Default Bool
}
@Test func testRelaxedMockWithStubbing() async throws {
let mock = MockCalculatorService(mode: .relaxed)
await every { mock.add(a: 1, b: 2) }.returns(100)
// Stubbed call returns stubbed value
let stubbed = mock.add(a: 1, b: 2)
#expect(stubbed == 100)
// Unstubbed call returns default value
let unstubbed = mock.add(a: 5, b: 10)
#expect(unstubbed == 0)
}SwiftMockk provides convenience methods for stubbing methods that return Result<Success, Failure>:
public enum NetworkError: Error, Equatable {
case timeout
case serverError
}
// swiftmockk:generate
public protocol NetworkService {
func fetch(url: String) -> Result<Data, NetworkError>
}
@Test func testResultTypeSuccess() async throws {
let mock = MockNetworkService()
let testData = Data([1, 2, 3, 4])
// Use convenience method for success
await every { mock.fetch(url: any()) }.returnsSuccess(testData, failureType: NetworkError.self)
let result = mock.fetch(url: "https://example.com")
guard case .success(let data) = result else {
Issue.record("Expected success")
return
}
#expect(data == testData)
}
@Test func testResultTypeFailure() async throws {
let mock = MockNetworkService()
// Use convenience method for failure
await every { mock.fetch(url: any()) }.returnsFailure(NetworkError.timeout, successType: Data.self)
let result = mock.fetch(url: "https://example.com")
guard case .failure(let error) = result else {
Issue.record("Expected failure")
return
}
#expect(error == .timeout)
}
@Test func testResultWithExplicitConstruction() async throws {
let mock = MockNetworkService()
let testData = Data([1, 2, 3, 4])
// Or use explicit Result construction
let success: Result<Data, NetworkError> = .success(testData)
await every { mock.fetch(url: "test") }.returns(success)
let result = mock.fetch(url: "test")
guard case .success(let data) = result else {
Issue.record("Expected success")
return
}
#expect(data == testData)
}Note: Due to Swift's type inference limitations, both convenience methods require explicit type parameters:
returnsSuccess(_:failureType:)- requires the failure error typereturnsFailure(_:successType:)- requires the success value type
SwiftMockk supports Swift 6's typed throws syntax. When a protocol method uses typed throws, the generated mock preserves the error type:
public enum UserError: Error, Equatable {
case notFound
case invalidId
}
// Note: Swift 6+ typed throws syntax: throws(ErrorType)
// swiftmockk:generate
public protocol UserService {
func getUser(id: String) throws(UserError) -> User
func fetchUsers() async throws(UserError) -> [User]
}
@Test func testTypedThrows() async throws {
let mock = MockUserService()
// Stub to throw a specific error type
await every { try mock.getUser(id: any()) }.throws(UserError.notFound)
do {
_ = try mock.getUser(id: "123")
Issue.record("Expected UserError.notFound")
} catch let error as UserError {
#expect(error == .notFound)
}
}Important Notes on Typed Throws:
- Typed throws syntax
throws(ErrorType)requires Swift 6+ language mode - Typed throws methods MUST be stubbed: If a typed throws method is called without a stub, it will
fatalError()instead of throwingMockError.noStub. This is because Swift's typed throws cannot throwMockError- only the specific error type - When stubbing typed throws methods, the error you provide must match the error type (e.g.,
UserErrorforthrows(UserError)) - User-provided errors from stubs are automatically cast to the correct type
SwiftMockk provides comprehensive support for generics in protocols, including generic methods, generic protocols, and associated types.
Methods with type parameters are fully supported, including constraints and where clauses:
// swiftmockk:generate
protocol DataRepository {
func fetch<T: Decodable>() async throws -> T
func save<T: Encodable>(_ item: T) async throws
func process<T>(_ data: T) throws -> String where T: Codable & Sendable
}
@Test func testGenericMethod() async throws {
let mock = MockDataRepository()
struct User: Codable, Equatable {
let id: String
let name: String
}
let testUser = User(id: "123", name: "Alice")
// Stub with type inference
await every { try await mock.fetch() as User }.returns(testUser)
// Call with specific type
let result: User = try await mock.fetch()
#expect(result == testUser)
}Key Points:
- Type inference works naturally - specify the return type when stubbing
- Generic constraints (e.g.,
T: Decodable) are preserved - Where clauses are fully supported
Protocols with primary associated types (Swift 5.7+) are fully supported:
// swiftmockk:generate
protocol Repository<Entity> {
func fetch(id: String) async throws -> Entity
func save(_ entity: Entity) async throws
func delete(id: String) async throws
}
@Test func testGenericProtocol() async throws {
struct Product: Equatable {
let id: String
let name: String
}
// Instantiate with specific type
let productRepo = MockRepository<Product>()
let testProduct = Product(id: "p1", name: "Widget")
await every { try await productRepo.fetch(id: "p1") }.returns(testProduct)
let result = try await productRepo.fetch(id: "p1")
#expect(result == testProduct)
}Key Points:
- Works with primary associated types:
protocol Repository<Entity> - Create mocks with specific types:
MockRepository<Product>() - Can create multiple mocks with different types in the same test
Traditional associatedtype declarations are automatically converted to generic parameters:
// swiftmockk:generate
protocol Container {
associatedtype Item
func add(_ item: Item)
func getAll() -> [Item]
}
@Test func testAssociatedTypes() async throws {
// MockContainer<Item> is generated
let stringContainer = MockContainer<String>()
await every { stringContainer.getAll() }.returns(["Hello", "World"])
let result = stringContainer.getAll()
#expect(result == ["Hello", "World"])
}Key Points:
- Associated types are converted to generic parameters on the mock class
- Constraints on associated types are preserved as where clauses
- Multiple associated types are supported:
associatedtype Input+associatedtype Output
Protocols with multiple type parameters work seamlessly:
// swiftmockk:generate
protocol Cache<Key, Value> where Key: Hashable {
func get(_ key: Key) -> Value?
func set(_ key: Key, value: Value)
}
@Test func testMultipleTypeParameters() async throws {
let cache = MockCache<String, Int>()
await every { cache.get("answer") }.returns(42)
let result = cache.get("answer")
#expect(result == 42)
}Variadic generics with parameter packs are fully supported:
// swiftmockk:generate
protocol VariadicProcessor {
func process<each T>(_ values: repeat each T) -> (repeat each T)
}
@Test func testVariadicGenerics() async throws {
let mock = MockVariadicProcessor()
// Stub with multiple types
await every { mock.process("Hello", 42, true) }.returns(("Hello", 42, true))
let result = mock.process("Hello", 42, true)
#expect(result.0 == "Hello")
#expect(result.1 == 42)
#expect(result.2 == true)
}Note: Parameter pack arguments aren't recorded individually (due to Swift type erasure limitations), but stubbing and verification by method name work correctly.
- mockk() with stored property initializers: Due to Swift's lazy evaluation,
mockk()cannot be used as a stored property initializer without workarounds. See Using mockk() with Stored Property Initializers for solutions. - Generic protocols and mockk(): Generic protocols (those with associated types or type parameters) cannot use
mockk()- use direct instantiation instead:MockRepository<User>() - Typed throws methods must be stubbed: Unstubbed typed throws methods will
fatalError()instead of throwingMockError.noStub(see Typed Throws section above) - Relaxed mocks: Relaxed mode only works with primitive types (Int, String, Bool, etc.), not complex structs or Result types
- Spies: Not yet implemented (cannot call through to real implementations)
- Protocol-only: Can only mock protocols, not concrete classes
MIT License - See LICENSE file for details
Contributions are welcome! Please feel free to submit a Pull Request.
Inspired by mockk - the excellent mocking library for Kotlin