Skip to content

Commit

Permalink
Merge pull request #131 from Matejkob/access-level-argument
Browse files Browse the repository at this point in the history
Add access level argument
  • Loading branch information
Matejkob authored Dec 3, 2024
2 parents d9b6878 + 7aadab0 commit 5a8fa8d
Show file tree
Hide file tree
Showing 7 changed files with 422 additions and 203 deletions.
2 changes: 1 addition & 1 deletion Examples/Sources/ViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Spyable

@Spyable(behindPreprocessorFlag: "DEBUG")
@Spyable(behindPreprocessorFlag: "DEBUG", accessLevel: .public)
protocol ServiceProtocol {
var name: String { get }
var anyProtocol: any Codable { get set }
Expand Down
166 changes: 52 additions & 114 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,16 @@
[![](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.
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.

## 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.
Spyable enhances your Swift workflow with the following features:

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.
- **Automatic Spy Generation**: Annotate a protocol with `@Spyable`, and let the macro generate a corresponding spy class.
- **Access Level Inheritance**: The generated class automatically inherits the protocol's access level.
- **Explicit Access Control**: Use the `accessLevel` argument to override the inherited access level if needed.
- **Interaction Tracking**: For testing, the generated spy tracks method calls, arguments, and return values.

## Quick Start

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

```swift
@Spyable
protocol ServiceProtocol {
public 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.
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.

```swift
class ServiceProtocolSpy: ServiceProtocol {
var name: String {
public class ServiceProtocolSpy: ServiceProtocol {
public var name: String {
get { underlyingName }
set { underlyingName = newValue }
}
var underlyingName: (String)!
public var underlyingName: (String)!

var fetchConfigArgCallsCount = 0
var fetchConfigArgCalled: Bool {
public var fetchConfigArgCallsCount = 0
public 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] {
public var fetchConfigArgReceivedArg: UInt8?
public var fetchConfigArgReceivedInvocations: [UInt8] = []
public var fetchConfigArgThrowableError: (any Error)?
public var fetchConfigArgReturnValue: [String: String]!
public var fetchConfigArgClosure: ((UInt8) async throws -> [String: String])?

public func fetchConfig(arg: UInt8) async throws -> [String: String] {
fetchConfigArgCallsCount += 1
fetchConfigArgReceivedArg = (arg)
fetchConfigArgReceivedInvocations.append((arg))
Expand Down Expand Up @@ -91,129 +88,70 @@ func testFetchConfig() async throws {

## Advanced Usage

### Generic Functions
### Access Level Inheritance and Overrides

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

```swift
func foo<T, U>(_ bar: T) -> U
@Spyable
internal protocol InternalProtocol {
func doSomething()
}
```

Generates the following spy:
This generates:

```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<T, U>(_ bar: T) -> U {
fooCallsCount += 1
fooReceivedBar = (bar)
fooReceivedInvocations.append((bar))
if fooClosure != nil {
return fooClosure!(bar) as! U
} else {
return fooReturnValue as! U
}
}
internal class InternalProtocolSpy: InternalProtocol {
internal func doSomething() { ... }
}
```

#### 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**:
You can override this behavior by explicitly specifying an access level:

```swift
@Spyable
protocol ServiceProtocol {
func wrapDataInArray<T>(_ data: T) -> Array<T>
}

struct ViewModel {
let service: ServiceProtocol

func wrapData<T>(_ data: T) -> Array<T> {
service.wrapDataInArray(data)
}
@Spyable(accessLevel: .fileprivate)
public protocol CustomProtocol {
func restrictedTask()
}
```

Test for `wrapData()`:
Generates:

```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
fileprivate class CustomProtocolSpy: CustomProtocol {
fileprivate func restrictedTask() { ... }
}
```

> [!TIP]
> If you see a crash in the generic function, check the type alignment between expected and injected values.
Supported values for `accessLevel` are:
- `.public`
- `.package`
- `.internal`
- `.fileprivate`
- `.private`

### Restricting Spy Availability

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

```swift
@Spyable(behindPreprocessorFlag: "DEBUG")
protocol MyService {
func fetchData() async
protocol DebugProtocol {
func logSomething()
}
```

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]
Generates:

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
```swift
#if DEBUG
internal class DebugProtocolSpy: DebugProtocol {
internal func logSomething() { ... }
}
#endif
```

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
Expand Down
Loading

0 comments on commit 5a8fa8d

Please sign in to comment.