Skip to content

Commit 5a8fa8d

Browse files
authored
Merge pull request #131 from Matejkob/access-level-argument
Add access level argument
2 parents d9b6878 + 7aadab0 commit 5a8fa8d

File tree

7 files changed

+422
-203
lines changed

7 files changed

+422
-203
lines changed

Examples/Sources/ViewModel.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Spyable
22

3-
@Spyable(behindPreprocessorFlag: "DEBUG")
3+
@Spyable(behindPreprocessorFlag: "DEBUG", accessLevel: .public)
44
protocol ServiceProtocol {
55
var name: String { get }
66
var anyProtocol: any Codable { get set }

README.md

+52-114
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,16 @@
55
[![](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)
66
[![](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)
77

8-
Spyable is a powerful tool for Swift that simplifies and automates the process of creating spies for testing. By using
9-
the `@Spyable` annotation on a protocol, the macro generates a spy class that implements the same interface and tracks
10-
interactions with its methods and properties.
8+
Spyable is a powerful tool for Swift that automates the process of creating protocol-conforming classes. Initially designed to simplify testing by generating spies, it is now widely used for various scenarios, such as SwiftUI previews or creating quick dummy implementations.
119

1210
## Overview
1311

14-
A "spy" is a test double that replaces a real component and records all interactions for later inspection. It's
15-
particularly useful in behavior verification, where the interaction between objects is the subject of the test.
12+
Spyable enhances your Swift workflow with the following features:
1613

17-
The Spyable macro revolutionizes the process of creating spies in Swift testing:
18-
19-
- **Automatic Spy Generation**: Annotate a protocol with `@Spyable`, and let the macro generate the corresponding spy class.
20-
- **Interaction Tracking**: The generated spy records method calls, arguments, and return values, making it easy to verify behavior in your tests.
14+
- **Automatic Spy Generation**: Annotate a protocol with `@Spyable`, and let the macro generate a corresponding spy class.
15+
- **Access Level Inheritance**: The generated class automatically inherits the protocol's access level.
16+
- **Explicit Access Control**: Use the `accessLevel` argument to override the inherited access level if needed.
17+
- **Interaction Tracking**: For testing, the generated spy tracks method calls, arguments, and return values.
2118

2219
## Quick Start
2320

@@ -26,33 +23,33 @@ The Spyable macro revolutionizes the process of creating spies in Swift testing:
2623

2724
```swift
2825
@Spyable
29-
protocol ServiceProtocol {
26+
public protocol ServiceProtocol {
3027
var name: String { get }
3128
func fetchConfig(arg: UInt8) async throws -> [String: String]
3229
}
3330
```
3431

35-
This generates a spy class named `ServiceProtocolSpy` that implements `ServiceProtocol`. The generated class includes
36-
properties and methods for tracking method calls, arguments, and return values.
32+
This generates a spy class named `ServiceProtocolSpy` with a `public` access level. The generated class includes properties and methods for tracking method calls, arguments, and return values.
3733

3834
```swift
39-
class ServiceProtocolSpy: ServiceProtocol {
40-
var name: String {
35+
public class ServiceProtocolSpy: ServiceProtocol {
36+
public var name: String {
4137
get { underlyingName }
4238
set { underlyingName = newValue }
4339
}
44-
var underlyingName: (String)!
40+
public var underlyingName: (String)!
4541

46-
var fetchConfigArgCallsCount = 0
47-
var fetchConfigArgCalled: Bool {
42+
public var fetchConfigArgCallsCount = 0
43+
public var fetchConfigArgCalled: Bool {
4844
return fetchConfigArgCallsCount > 0
4945
}
50-
var fetchConfigArgReceivedArg: UInt8?
51-
var fetchConfigArgReceivedInvocations: [UInt8] = []
52-
var fetchConfigArgThrowableError: (any Error)?
53-
var fetchConfigArgReturnValue: [String: String]!
54-
var fetchConfigArgClosure: ((UInt8) async throws -> [String: String])?
55-
func fetchConfig(arg: UInt8) async throws -> [String: String] {
46+
public var fetchConfigArgReceivedArg: UInt8?
47+
public var fetchConfigArgReceivedInvocations: [UInt8] = []
48+
public var fetchConfigArgThrowableError: (any Error)?
49+
public var fetchConfigArgReturnValue: [String: String]!
50+
public var fetchConfigArgClosure: ((UInt8) async throws -> [String: String])?
51+
52+
public func fetchConfig(arg: UInt8) async throws -> [String: String] {
5653
fetchConfigArgCallsCount += 1
5754
fetchConfigArgReceivedArg = (arg)
5855
fetchConfigArgReceivedInvocations.append((arg))
@@ -91,129 +88,70 @@ func testFetchConfig() async throws {
9188

9289
## Advanced Usage
9390

94-
### Generic Functions
91+
### Access Level Inheritance and Overrides
9592

96-
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.
97-
98-
For example:
93+
By default, the generated spy inherits the access level of the annotated protocol. For example:
9994

10095
```swift
101-
func foo<T, U>(_ bar: T) -> U
96+
@Spyable
97+
internal protocol InternalProtocol {
98+
func doSomething()
99+
}
102100
```
103101

104-
Generates the following spy:
102+
This generates:
105103

106104
```swift
107-
class MyProtocolSpy: MyProtocol {
108-
var fooCallsCount = 0
109-
var fooCalled: Bool {
110-
return fooCallsCount > 0
111-
}
112-
var fooReceivedBar: Any?
113-
var fooReceivedInvocations: [Any] = []
114-
var fooReturnValue: Any!
115-
var fooClosure: ((Any) -> Any)?
116-
117-
func foo<T, U>(_ bar: T) -> U {
118-
fooCallsCount += 1
119-
fooReceivedBar = (bar)
120-
fooReceivedInvocations.append((bar))
121-
if fooClosure != nil {
122-
return fooClosure!(bar) as! U
123-
} else {
124-
return fooReturnValue as! U
125-
}
126-
}
105+
internal class InternalProtocolSpy: InternalProtocol {
106+
internal func doSomething() { ... }
127107
}
128108
```
129109

130-
#### Important Notes:
131-
132-
1. **Type Matching**:
133-
Ensure the expected types align with the injected `returnValue` or `closure`. Mismatched types will result in runtime crashes due to force casting.
134-
135-
2. **Example**:
110+
You can override this behavior by explicitly specifying an access level:
136111

137112
```swift
138-
@Spyable
139-
protocol ServiceProtocol {
140-
func wrapDataInArray<T>(_ data: T) -> Array<T>
141-
}
142-
143-
struct ViewModel {
144-
let service: ServiceProtocol
145-
146-
func wrapData<T>(_ data: T) -> Array<T> {
147-
service.wrapDataInArray(data)
148-
}
113+
@Spyable(accessLevel: .fileprivate)
114+
public protocol CustomProtocol {
115+
func restrictedTask()
149116
}
150117
```
151118

152-
Test for `wrapData()`:
119+
Generates:
153120

154121
```swift
155-
func testWrapData() {
156-
serviceSpy.wrapDataInArrayReturnValue = [123]
157-
XCTAssertEqual(sut.wrapData(1), [123])
158-
XCTAssertEqual(serviceSpy.wrapDataInArrayReceivedData as? Int, 1)
159-
160-
// Incorrect usage: mismatched type
161-
// serviceSpy.wrapDataInArrayReturnValue = ["hello"] // ⚠️ Causes runtime error
122+
fileprivate class CustomProtocolSpy: CustomProtocol {
123+
fileprivate func restrictedTask() { ... }
162124
}
163125
```
164126

165-
> [!TIP]
166-
> If you see a crash in the generic function, check the type alignment between expected and injected values.
127+
Supported values for `accessLevel` are:
128+
- `.public`
129+
- `.package`
130+
- `.internal`
131+
- `.fileprivate`
132+
- `.private`
167133

168134
### Restricting Spy Availability
169135

170-
You can limit where Spyable's generated code can be used by using the `behindPreprocessorFlag` parameter:
136+
Use the `behindPreprocessorFlag` parameter to wrap the generated code in a preprocessor directive:
171137

172138
```swift
173139
@Spyable(behindPreprocessorFlag: "DEBUG")
174-
protocol MyService {
175-
func fetchData() async
140+
protocol DebugProtocol {
141+
func logSomething()
176142
}
177143
```
178144

179-
This wraps the generated spy in an `#if DEBUG` preprocessor macro, preventing its use where the `DEBUG` flag is not defined.
180-
181-
> [!IMPORTANT]
182-
> The `behindPreprocessorFlag` argument must be a static string literal.
183-
184-
### Xcode Previews Consideration
185-
186-
If you need spies in Xcode Previews while excluding them from production builds, consider using a custom compilation flag (e.g., `SPIES_ENABLED`):
187-
188-
The following diagram illustrates how to set up your project structure with the `SPIES_ENABLED` flag:
189-
190-
```mermaid
191-
graph TD
192-
A[MyFeature] --> B[MyFeatureTests]
193-
A --> C[MyFeaturePreviews]
194-
195-
A -- SPIES_ENABLED = 0 --> D[Production Build]
196-
B -- SPIES_ENABLED = 1 --> E[Test Build]
197-
C -- SPIES_ENABLED = 1 --> F[Preview Build]
145+
Generates:
198146

199-
style A fill:#ff9999,stroke:#333,stroke-width:2px,color:#000
200-
style B fill:#99ccff,stroke:#333,stroke-width:2px,color:#000
201-
style C fill:#99ffcc,stroke:#333,stroke-width:2px,color:#000
202-
style D fill:#ffcc99,stroke:#333,stroke-width:2px,color:#000
203-
style E fill:#99ccff,stroke:#333,stroke-width:2px,color:#000
204-
style F fill:#99ffcc,stroke:#333,stroke-width:2px,color:#000
147+
```swift
148+
#if DEBUG
149+
internal class DebugProtocolSpy: DebugProtocol {
150+
internal func logSomething() { ... }
151+
}
152+
#endif
205153
```
206154

207-
Set this flag under "Active Compilation Conditions" for both test and preview targets.
208-
209-
## Examples
210-
211-
Find examples of how to use Spyable [here](./Examples).
212-
213-
## Documentation
214-
215-
The latest documentation is available [here](https://swiftpackageindex.com/Matejkob/swift-spyable/0.1.2/documentation/spyable).
216-
217155
## Installation
218156

219157
### Xcode Projects

0 commit comments

Comments
 (0)