Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/Matejkob/swift-spyable
Swift macro that simplifies and automates the process of creating spies for testing
https://github.com/Matejkob/swift-spyable
software-quality swift swift-macros testing unit-testing
Last synced: 29 days ago
JSON representation
Swift macro that simplifies and automates the process of creating spies for testing
- Host: GitHub
- URL: https://github.com/Matejkob/swift-spyable
- Owner: Matejkob
- License: mit
- Created: 2023-06-09T16:48:20.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2024-03-05T19:33:37.000Z (10 months ago)
- Last Synced: 2024-04-25T19:21:18.808Z (8 months ago)
- Topics: software-quality, swift, swift-macros, testing, unit-testing
- Language: Swift
- Homepage:
- Size: 266 KB
- Stars: 372
- Watchers: 5
- Forks: 28
- Open Issues: 14
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# Spyable
[![GitHub Workflow Status](https://github.com/Matejkob/swift-spyable/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Matejkob/swift-spyable/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/Matejkob/swift-spyable/graph/badge.svg?token=YRMM1BDQ85)](https://codecov.io/gh/Matejkob/swift-spyable)
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FMatejkob%2Fswift-spyable%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/Matejkob/swift-spyable)
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FMatejkob%2Fswift-spyable%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/Matejkob/swift-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
1. Import Spyable: `import Spyable`
2. Annotate your protocol with `@Spyable`:```swift
@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.```swift
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
}
}
}
```3. Use the spy in your tests:
```swift
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
### Generic Functions
Spyable supports generic functions, but their implementation involves special handling. Due to limitations in Swift, generic parameters in a function are replaced with `Any` in the spy class to store arguments, return values, and closures.
For example:
```swift
func foo(_ bar: T) -> U
```Generates the following spy:
```swift
class MyProtocolSpy: MyProtocol {
var fooCallsCount = 0
var fooCalled: Bool {
return fooCallsCount > 0
}
var fooReceivedBar: Any?
var fooReceivedInvocations: [Any] = []
var fooReturnValue: Any!
var fooClosure: ((Any) -> Any)?func foo(_ bar: T) -> U {
fooCallsCount += 1
fooReceivedBar = (bar)
fooReceivedInvocations.append((bar))
if fooClosure != nil {
return fooClosure!(bar) as! U
} else {
return fooReturnValue as! U
}
}
}
```#### Important Notes:
1. **Type Matching**:
Ensure the expected types align with the injected `returnValue` or `closure`. Mismatched types will result in runtime crashes due to force casting.2. **Example**:
```swift
@Spyable
protocol ServiceProtocol {
func wrapDataInArray(_ data: T) -> Array
}struct ViewModel {
let service: ServiceProtocolfunc wrapData(_ data: T) -> Array {
service.wrapDataInArray(data)
}
}
```Test for `wrapData()`:
```swift
func testWrapData() {
serviceSpy.wrapDataInArrayReturnValue = [123]
XCTAssertEqual(sut.wrapData(1), [123])
XCTAssertEqual(serviceSpy.wrapDataInArrayReceivedData as? Int, 1)// Incorrect usage: mismatched type
// serviceSpy.wrapDataInArrayReturnValue = ["hello"] // ⚠️ Causes runtime error
}
```> [!TIP]
> If you see a crash in the generic function, check the type alignment between expected and injected values.### Restricting Spy Availability
You can limit where Spyable's generated code can be used by using the `behindPreprocessorFlag` parameter:
```swift
@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:
```mermaid
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](./Examples).
## Documentation
The latest documentation is available [here](https://swiftpackageindex.com/Matejkob/swift-spyable/0.1.2/documentation/spyable).
## Installation
### Xcode Projects
Add Spyable as a package dependency:
```
https://github.com/Matejkob/swift-spyable
```### Swift Package Manager
Add to your `Package.swift`:
```swift
dependencies: [
.package(url: "https://github.com/Matejkob/swift-spyable", from: "0.3.0")
]
```Then, add the product to your target:
```swift
.product(name: "Spyable", package: "swift-spyable"),
```## License
This library is released under the MIT license. See [LICENSE](LICENSE) for details.