Hey folks!
I'm thrilled to introduce Spyable - a powerful Swift macro that simplifies and automates the process of creating spies for testing.
My goal with Spyable is to make it easier to write comprehensive, precise tests, and ensure our applications behave exactly as we expect them to.
I believe this macro can become a replacement for the most popular use-case of Sourcery - Mocks & Stubs. In order to ensure a smooth and seamless transition from Sourcery to the Spyable macro, I have taken steps in the initial version of Spyable. It's designed to generate spies for protocols in the same way as the AutoMockable template. Thanks to that, most projects using Sourcery with the basic AutoMockable template can be switched in minutes.
As many of you know, creating spies manually is tedious, and error-prone. With Spyable, you only need to annotate your protocol with @Spyable
. The macro will then generate a corresponding spy class that implement this protocol. This generated class can track all interactions with its methods and properties. It records method invocations, their arguments, and returned values, which are then available for you to make precise assertions about the behavior of your system under test.
Here's a simple example of what this looks like:
Example
@Spyable
protocol ServiceProtocol {
func fetchConfig(_ arg: UInt8) async throws -> [String: String]
}
This would generate a ServiceProtocolSpy
class that implements ServiceProtocol
, and includes properties and methods for tracking interactions.
class ServiceProtocolSpy: ServiceProtocol {
var fetchConfigCallsCount = 0
var fetchConfigCalled: Bool {
return fetchConfigCallsCount > 0
}
var fetchConfigReceivedArg: UInt8?
var fetchConfigReceivedInvocations: [UInt8] = []
var fetchConfigReturnValue: [String: String]!
var fetchConfigClosure: ((UInt8) async throws -> [String: String])?
func fetchConfig(_ arg: UInt8) async throws -> [String: String] {
fetchConfigCallsCount += 1
fetchConfigReceivedArg = (arg)
fetchConfigReceivedInvocations.append((arg))
if fetchConfigClosure != nil {
return try await fetchConfigClosure!(arg)
} else {
return fetchConfigReturnValue
}
}
}
And in your tests, you can use the spy to verify that your code is interacting correctly:
func testFetchConfig() async throws {
let serviceSpy = ServiceProtocolSpy()
let sut = ViewModel(service: serviceSpy)
serviceSpy.fetchConfigWithArgReturnValue = ["key": "value"]
try await sut.fetchConfig()
XCTAssertEqual(serviceSpy.fetchConfigWithArgCallsCount, 1)
XCTAssertEqual(serviceSpy.fetchConfigWithArgReceivedInvocations, [1])
try await sut.saveConfig()
XCTAssertEqual(serviceSpy.fetchConfigWithArgCallsCount, 2)
XCTAssertEqual(serviceSpy.fetchConfigWithArgReceivedInvocations, [1, 1])
}
I encourage you all to try Spyable and to contribute to its development on GitHub. I would love to hear about your experiences, the pros and cons you've found, and any suggestions for improvements you might have. With your input, I believe we can make Spyable an even more robust tool for Swift testing.
Happy Coding!