Skip to content

Commit

Permalink
Merge pull request #102 from Matejkob/update-readme
Browse files Browse the repository at this point in the history
Update README
  • Loading branch information
Matejkob authored Jun 26, 2024
2 parents 3f4a84f + 7f9c638 commit fa32cbb
Showing 1 changed file with 82 additions and 108 deletions.
190 changes: 82 additions & 108 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,188 +5,162 @@
[![](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)

A powerful tool for Swift that simplifies and automates the process of creating spies
for testing. Using the `@Spyable` annotation on a protocol, the macro generates
a spy class that implements the same interface as the protocol and keeps track of
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 specific type of test double that not only replaces a real component, but also
records all interactions for later inspection. It's particularly useful in behavior verification,
where the interaction between objects, rather than the state, is the subject of the test.
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 is designed to simplify and enhance the usage of spies in Swift testing.
Traditionally, developers would need to manually create spies for each protocol in their
codebase — a tedious and error-prone task. The Spyable macro revolutionizes this process
by automatically generating these spies.
The Spyable macro revolutionizes the process of creating spies in Swift testing:

When a protocol is annotated with `@Spyable`, the macro generates a corresponding spy class that
implement this protocol. This spy class is capable of tracking all interactions with its methods
and properties. It records method invocations, their arguments, and returned values, providing
a comprehensive log of interactions that occurred during the test. This data can then be used
to make precise assertions about the behavior of the system under test.
- **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.

**TL;DR**
## Quick Start

The Spyable macro provides the following functionality:
- **Automatic Spy Generation**: No need to manually create spy classes for each protocol.
Just annotate the protocol with `@Spyable`, and let the macro do the rest.
- **Interaction Tracking**: The generated spy records method calls, arguments, and return
values, making it easy to verify behavior in your tests.
- **Swift Syntax**: The macro uses Swift syntax, providing a seamless and familiar experience
for Swift developers.

## Quick start

To get started, import Spyable: `import Spyable`, annotate your protocol with `@Spyable`:
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]
var name: String { get }
func fetchConfig(arg: UInt8) async throws -> [String: String]
}
```

This will generate a spy class named `ServiceProtocolSpy` that implements `ServiceProtocol`.
The generated class includes properties and methods for tracking the number of method calls,
the arguments passed, and whether the method was called.
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 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
}
var underlyingName: (String)!

var fetchConfigArgCallsCount = 0
var fetchConfigArgCalled: Bool {
return fetchConfigArgCallsCount > 0
}
var fetchConfigArgReceivedArg: UInt8?
var fetchConfigArgReceivedInvocations: [UInt8] = []
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 fetchConfigArgClosure != nil {
return try await fetchConfigArgClosure!(arg)
} else {
return fetchConfigArgReturnValue
}
if fetchConfigArgClosure != nil {
return try await fetchConfigArgClosure!(arg)
} else {
return fetchConfigArgReturnValue
}
}
}
```

Then, in your tests, you can use the spy to verify that your code is interacting
with the `service` dependency of type `ServiceProtocol` correctly:
3. Use the spy in your tests:

```swift
func testFetchConfig() async throws {
let serviceSpy = ServiceProtocolSpy()
let sut = ViewModel(service: serviceSpy)
let serviceSpy = ServiceProtocolSpy()
let sut = ViewModel(service: serviceSpy)

serviceSpy.fetchConfigArgReturnValue = ["key": "value"]
serviceSpy.fetchConfigArgReturnValue = ["key": "value"]

try await sut.fetchConfig()
try await sut.fetchConfig()

XCTAssertEqual(serviceSpy.fetchConfigArgCallsCount, 1)
XCTAssertEqual(serviceSpy.fetchConfigArgReceivedInvocations, [1])
XCTAssertEqual(serviceSpy.fetchConfigArgCallsCount, 1)
XCTAssertEqual(serviceSpy.fetchConfigArgReceivedInvocations, [1])

try await sut.saveConfig()
try await sut.saveConfig()

XCTAssertEqual(serviceSpy.fetchConfigArgCallsCount, 2)
XCTAssertEqual(serviceSpy.fetchConfigArgReceivedInvocations, [1, 1])
XCTAssertEqual(serviceSpy.fetchConfigArgCallsCount, 2)
XCTAssertEqual(serviceSpy.fetchConfigArgReceivedInvocations, [1, 1])
}
```

## Advanced Usage

### Restricting the Availability of Spies

If you wish, you can limit where `Spyable`'s generated code can be used from. This can be useful if you want to prevent spies from being used in user-facing production code.
### Restricting Spy Availability

To apply this conditional compilation, use the `behindPreprocessorFlag: String?` parameter within the `@Spyable` attribute. This parameter accepts a **static string literal** representing the compilation flag that controls the inclusion of the generated spy code.

Example usage with the `DEBUG` flag:
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
func fetchData() async
}
```

With `behindPreprocessorFlag` specified as `DEBUG`, the macro expansion will be wrapped in an `#if DEBUG` preprocessor macro, preventing its use anywhere that the `DEBUG` flag is not defined:

```swift
#if DEBUG
class MyServiceSpy: MyService {
var fetchDataCallsCount = 0
var fetchDataCalled: Bool {
return fetchDataCallsCount > 0
}
var fetchDataClosure: (() async -> Void)?

func fetchData() async {
fetchDataCallsCount += 1
await fetchDataClosure?()
}
}
#endif
```

This approach allows for great flexibility, enabling you to define any flag (e.g., `TESTS`) and configure your build settings to include the spy code only in specific targets (like test or preview targets) by defining the appropriate flag under "Active Compilation Conditions" in your project's build settings.
This wraps the generated spy in an `#if DEBUG` preprocessor macro, preventing its use where the `DEBUG` flag is not defined.

> [!IMPORTANT]
> When specifying the `behindPreprocessorFlag` argument, it is crucial to use a static string literal. This requirement ensures that the preprocessor flag's integrity is maintained and that conditional compilation behaves as expected. The Spyable system will provide a diagnostic message if the argument does not meet this requirement, guiding you to correct the implementation.
> The `behindPreprocessorFlag` argument must be a static string literal.
#### A Caveat Regarding Xcode Previews
### Xcode Previews Consideration

Limiting the availability of spy implementations through conditional compilation can impact the usability of spies in Xcode Previews. If you rely on spies within your previews while also wanting to exclude them from production builds, consider defining a separate compilation flag (e.g., `SPIES_ENABLED`) for preview and test targets:
If you need spies in Xcode Previews while excluding them from production builds, consider using a custom compilation flag (e.g., `SPIES_ENABLED`):

```
-- MyFeature (`SPIES_ENABLED = 0`)
---- MyFeatureTests (`SPIES_ENABLED = 1`)
---- MyFeaturePreviews (`SPIES_ENABLED = 1`)
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:#f9f,stroke:#333,stroke-width:2px
style B fill:#bbf,stroke:#333,stroke-width:2px
style C fill:#bfb,stroke:#333,stroke-width:2px
style D fill:#fbb,stroke:#333,stroke-width:2px
style E fill:#bbf,stroke:#333,stroke-width:2px
style F fill:#bfb,stroke:#333,stroke-width:2px
```

Set this custom flag under the "Active Compilation Conditions" for both your `MyFeatureTests` and `MyFeaturePreviews` targets to seamlessly integrate spy functionality where needed without affecting production code.
Set this flag under "Active Compilation Conditions" for both test and preview targets.

## Examples

This repo comes with an example of how to use Spyable. You can find it [here](./Examples).
Find examples of how to use Spyable [here](./Examples).

## Documentation

The latest documentation for this library is available [here](https://swiftpackageindex.com/Matejkob/swift-spyable/0.1.2/documentation/spyable).
The latest documentation is available [here](https://swiftpackageindex.com/Matejkob/swift-spyable/0.1.2/documentation/spyable).

## Installation

### Integrating with Xcode Projects
### Xcode Projects

To incorporate Spyable into your Xcode project, add it as a package dependency:
Add Spyable as a package dependency:

```
https://github.com/Matejkob/swift-spyable
```

### Using with Swift Package Manager (SwiftPM)
### Swift Package Manager

For projects utilizing Swift Package Manager, you can include Spyable by modifying your `Package.swift` file. Add the following to your `dependencies` array:
Add to your `Package.swift`:

```swift
dependencies: [
.package(url: "https://github.com/Matejkob/swift-spyable", from: "0.3.0")
]
```

Next, to use Spyable in your project, append the product to the appropriate target(s) in your `Package.swift`:
Then, add the product to your target:

```swift
.product(name: "Spyable", package: "swift-spyable"),
Expand Down

0 comments on commit fa32cbb

Please sign in to comment.