Awesome
Spyable
Spyable is a powerful tool for Swift that simplifies and automates the process of creating spies for testing. By using
the @Spyable
annotation on a protocol, the macro generates a spy class that implements the same interface and tracks
interactions with its methods and properties.
Overview
A "spy" is a test double that replaces a real component and records all interactions for later inspection. It's particularly useful in behavior verification, where the interaction between objects is the subject of the test.
The Spyable macro revolutionizes the process of creating spies in Swift testing:
- Automatic Spy Generation: Annotate a protocol with
@Spyable
, and let the macro generate the corresponding spy class. - Interaction Tracking: The generated spy records method calls, arguments, and return values, making it easy to verify behavior in your tests.
Quick Start
- Import Spyable:
import Spyable
- Annotate your protocol with
@Spyable
:
@Spyable
protocol ServiceProtocol {
var name: String { get }
func fetchConfig(arg: UInt8) async throws -> [String: String]
}
This generates a spy class named ServiceProtocolSpy
that implements ServiceProtocol
. The generated class includes
properties and methods for tracking method calls, arguments, and return values.
class ServiceProtocolSpy: ServiceProtocol {
var name: String {
get { underlyingName }
set { underlyingName = newValue }
}
var underlyingName: (String)!
var fetchConfigArgCallsCount = 0
var fetchConfigArgCalled: Bool {
return fetchConfigArgCallsCount > 0
}
var fetchConfigArgReceivedArg: UInt8?
var fetchConfigArgReceivedInvocations: [UInt8] = []
var fetchConfigArgThrowableError: (any Error)?
var fetchConfigArgReturnValue: [String: String]!
var fetchConfigArgClosure: ((UInt8) async throws -> [String: String])?
func fetchConfig(arg: UInt8) async throws -> [String: String] {
fetchConfigArgCallsCount += 1
fetchConfigArgReceivedArg = (arg)
fetchConfigArgReceivedInvocations.append((arg))
if let fetchConfigArgThrowableError {
throw fetchConfigArgThrowableError
}
if fetchConfigArgClosure != nil {
return try await fetchConfigArgClosure!(arg)
} else {
return fetchConfigArgReturnValue
}
}
}
- Use the spy in your tests:
func testFetchConfig() async throws {
let serviceSpy = ServiceProtocolSpy()
let sut = ViewModel(service: serviceSpy)
serviceSpy.fetchConfigArgReturnValue = ["key": "value"]
try await sut.fetchConfig()
XCTAssertEqual(serviceSpy.fetchConfigArgCallsCount, 1)
XCTAssertEqual(serviceSpy.fetchConfigArgReceivedInvocations, [1])
try await sut.saveConfig()
XCTAssertEqual(serviceSpy.fetchConfigArgCallsCount, 2)
XCTAssertEqual(serviceSpy.fetchConfigArgReceivedInvocations, [1, 1])
}
Advanced Usage
Restricting Spy Availability
You can limit where Spyable's generated code can be used by using the behindPreprocessorFlag
parameter:
@Spyable(behindPreprocessorFlag: "DEBUG")
protocol MyService {
func fetchData() async
}
This wraps the generated spy in an #if DEBUG
preprocessor macro, preventing its use where the DEBUG
flag is not defined.
[!IMPORTANT] The
behindPreprocessorFlag
argument must be a static string literal.
Xcode Previews Consideration
If you need spies in Xcode Previews while excluding them from production builds, consider using a custom compilation flag (e.g., SPIES_ENABLED
):
The following diagram illustrates how to set up your project structure with the SPIES_ENABLED
flag:
graph TD
A[MyFeature] --> B[MyFeatureTests]
A --> C[MyFeaturePreviews]
A -- SPIES_ENABLED = 0 --> D[Production Build]
B -- SPIES_ENABLED = 1 --> E[Test Build]
C -- SPIES_ENABLED = 1 --> F[Preview Build]
style A fill:#ff9999,stroke:#333,stroke-width:2px,color:#000
style B fill:#99ccff,stroke:#333,stroke-width:2px,color:#000
style C fill:#99ffcc,stroke:#333,stroke-width:2px,color:#000
style D fill:#ffcc99,stroke:#333,stroke-width:2px,color:#000
style E fill:#99ccff,stroke:#333,stroke-width:2px,color:#000
style F fill:#99ffcc,stroke:#333,stroke-width:2px,color:#000
Set this flag under "Active Compilation Conditions" for both test and preview targets.
Examples
Find examples of how to use Spyable here.
Documentation
The latest documentation is available here.
Installation
Xcode Projects
Add Spyable as a package dependency:
https://github.com/Matejkob/swift-spyable
Swift Package Manager
Add to your Package.swift
:
dependencies: [
.package(url: "https://github.com/Matejkob/swift-spyable", from: "0.3.0")
]
Then, add the product to your target:
.product(name: "Spyable", package: "swift-spyable"),
License
This library is released under the MIT license. See LICENSE for details.