Skip to content

Commit 1ec968d

Browse files
authored
Leif/associated types (#9)
* Combine windows os code * Add support for associated types and classes * Update tests and documentation * Simplify type sorting
1 parent 3ed3df1 commit 1ec968d

File tree

6 files changed

+190
-79
lines changed

6 files changed

+190
-79
lines changed

Package.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ let GUISwiftSettings: [SwiftSetting] = [
1919
]
2020
let GUILinkerSettings: [LinkerSetting] = [
2121
]
22-
#endif
2322

24-
#if os(Windows)
2523
let macroTarget: Target = Target.macro(
2624
name: "MockedMacros",
2725
dependencies: [

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,39 @@ let mock = MockedCustomProtocol(
9191
print(mock.defaultMethod()) // Output: "default"
9292
```
9393

94+
### Advanced Usage
95+
96+
The `Mocked` macro can be used with more complex protocols, including those with associated types, `async` methods, `throws` methods, or a combination of both.
97+
98+
```swift
99+
@Mocked
100+
protocol ComplexProtocol {
101+
associatedtype ItemType
102+
associatedtype ItemValue: Codable
103+
func fetchData() async throws -> ItemType
104+
func processData(input: Int) -> Bool
105+
func storeValue(value: ItemValue) -> Void
106+
}
107+
108+
let mock = MockedComplexProtocol<String, Int>(
109+
fetchData: { return "Mocked Data" },
110+
processData: { input in return input > 0 }
111+
)
112+
113+
// Usage in a test
114+
Task {
115+
do {
116+
let data = try await mock.fetchData()
117+
print(data) // Output: "Mocked Data"
118+
} catch {
119+
XCTFail("Unexpected error: \(error)")
120+
}
121+
}
122+
123+
let isValid = mock.processData(input: 5)
124+
XCTAssertTrue(isValid)
125+
```
126+
94127
### Edge Cases and Warnings
95128

96129
- **Non-Protocol Usage**: The `@Mocked` macro can only be applied to protocols. Using it on other types will result in a compilation error.

Sources/Mocked/Mocked.swift

Lines changed: 106 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,109 @@
1-
/// The `Mocked` macro is used to automatically generate a mocked implementation of a protocol.
2-
///
3-
/// This macro attaches a peer struct prefixed with `Mocked` that provides implementations of all the methods and properties defined in the protocol.
4-
///
5-
/// # Usage
6-
/// Apply the `@Mocked` attribute to a protocol declaration to generate a mock implementation of that protocol. This mock implementation can be used for unit testing purposes to easily verify interactions with the protocol methods and properties.
7-
///
8-
/// Example:
9-
/// ```swift
10-
/// @Mocked
11-
/// protocol MyProtocol {
12-
/// var title: String { get set }
13-
/// func performAction() -> Void
14-
/// }
15-
/// ```
16-
///
17-
/// The code above will generate a `MockedMyProtocol` struct that implements `MyProtocol`.
18-
///
19-
/// # Edge Cases and Warnings
20-
/// - **Non-Protocol Usage**: This macro can only be applied to protocol definitions. Attempting to use it on other types, such as classes or structs, will lead to a compilation error.
21-
/// - **Unimplemented Methods**: Any method that is not explicitly overridden will call `fatalError()` when invoked, which will crash the program. Ensure all necessary methods are mocked when using the generated struct.
22-
/// - **Async and Throwing Methods**: The macro correctly handles protocols with `async` and/or `throws` functions. Be mindful to provide appropriate closures during initialization.
23-
///
24-
/// # Example of Generated Code
25-
/// For the protocol `MyProtocol`, the generated mock implementation would look like this:
26-
/// ```swift
27-
/// struct MockedMyProtocol: MyProtocol {
28-
/// var title: String
29-
/// private let performActionOverride: (() -> Void)?
30-
///
31-
/// init(title: String, performAction: (() -> Void)? = nil) {
32-
/// self.title = title
33-
/// self.performActionOverride = performAction
34-
/// }
35-
///
36-
/// func performAction() {
37-
/// guard let performActionOverride else {
38-
/// fatalError("Mocked performAction was not implemented!")
39-
/// }
40-
/// performActionOverride()
41-
/// }
42-
/// }
43-
/// ```
1+
/**
2+
The `Mocked` macro is used to automatically generate a mocked implementation of a protocol, including support for associated types and automatic detection of class requirements.
3+
4+
This macro attaches a peer struct or class prefixed with `Mocked`, which provides implementations of all the methods and properties defined in the protocol. This is particularly useful for unit testing, where creating mock objects manually can be cumbersome and error-prone. With `@Mocked`, developers can easily generate mock implementations that allow precise control over protocol methods and properties, enabling more effective and focused testing.
5+
6+
# Usage
7+
Apply the `@Mocked` attribute to a protocol declaration to generate a mock implementation of that protocol. The generated mock will have the same properties and methods as the protocol, but they can be overridden through closures provided during initialization. This mock implementation can be used for unit testing purposes to easily verify interactions with the protocol methods and properties.
8+
9+
Example:
10+
```swift
11+
@Mocked
12+
protocol MyProtocol {
13+
var title: String { get set }
14+
func performAction() -> Void
15+
}
16+
```
17+
18+
The code above will generate a `MockedMyProtocol` struct that implements `MyProtocol`. This struct allows defining the behavior of `performAction()` by providing a closure during initialization, making it easy to set up test scenarios without writing extensive boilerplate code.
19+
20+
# Features
21+
The `@Mocked` macro provides several key features:
22+
23+
- **Automatic Mock Generation**: Generates a mock implementation for any protocol, saving time and reducing boilerplate code.
24+
- **Closure-Based Method Overrides**: Methods and properties can be overridden by providing closures during mock initialization, giving you full control over method behavior in different test scenarios.
25+
- **Support for Associated Types**: Handles protocols with associated types by using Swift generics, providing flexibility for complex protocol requirements.
26+
- **Automatic Detection of Class Requirements**: If the protocol conforms to `AnyObject`, the macro generates a class instead of a struct, ensuring reference semantics are maintained where needed.
27+
- **Support for `async` and `throws` Methods**: The generated mock can handle methods marked as `async` or `throws`, allowing you to create mock behaviors that include asynchronous operations or errors.
28+
- **Automatic Default Property Implementations**: Provides straightforward storage for properties defined in the protocol, which can be accessed and modified as needed.
29+
30+
# Edge Cases and Warnings
31+
- **Non-Protocol Usage**: This macro can only be applied to protocol definitions. Attempting to use it on other types, such as classes or structs, will lead to a compilation error.
32+
- **Unimplemented Methods**: Any method that is not explicitly overridden will call `fatalError()` when invoked, which will crash the program. This behavior is intentional to alert developers that the method was called without being properly mocked. Always ensure that all necessary methods are mocked when using the generated struct to avoid runtime crashes. Mocks should only be used in tests or previews, where such crashes are acceptable for ensuring proper setup.
33+
- **Async and Throwing Methods**: Be mindful to provide appropriate closures during initialization to match the behavior of `async` or `throws` methods. If no closure is provided, the default behavior will result in a `fatalError()`.
34+
- **Value vs. Reference Semantics**: The generated mock defaults to being a struct, which means it follows value semantics. If the protocol requires reference semantics (e.g., it conforms to `AnyObject`), the macro will generate a class instead.
35+
36+
# Example of Generated Code
37+
For the protocol `MyProtocol`, the generated mock implementation would look like this:
38+
```swift
39+
struct MockedMyProtocol: MyProtocol {
40+
// Properties defined by the protocol
41+
var title: String
42+
43+
// Closure to override the behavior of `performAction()`
44+
private let performActionOverride: (() -> Void)?
45+
46+
// Initializer to provide custom behavior for each method or property
47+
init(title: String, performAction: (() -> Void)? = nil) {
48+
self.title = title
49+
self.performActionOverride = performAction
50+
}
51+
52+
// Method implementation that uses the provided closure or triggers a `fatalError`
53+
func performAction() {
54+
guard let performActionOverride else {
55+
fatalError("Mocked performAction was not implemented!")
56+
}
57+
performActionOverride()
58+
}
59+
}
60+
```
61+
62+
In the generated code:
63+
- The `title` property is stored directly within the struct, allowing you to get or set its value just like a normal property.
64+
- The `performAction` method uses a closure (`performActionOverride`) provided during initialization. If no closure is provided, calling `performAction()` will result in a `fatalError`, ensuring you never accidentally call an unmocked method.
65+
66+
# Advanced Usage
67+
The `Mocked` macro can be used with more complex protocols, including those with associated types, `async` methods, `throws` methods, or a combination of both. This allows developers to test various scenarios, such as successful asynchronous operations or handling errors, without needing to write dedicated mock classes manually.
68+
69+
```swift
70+
@Mocked
71+
protocol ComplexProtocol {
72+
associatedtype ItemType
73+
associatedtype ItemValue: Codable
74+
func fetchData() async throws -> ItemType
75+
func processData(input: Int) -> Bool
76+
func storeValue(value: ItemValue) -> Void
77+
}
78+
79+
let mock = MockedComplexProtocol<String, Int>(
80+
fetchData: { return "Mocked Data" },
81+
processData: { input in return input > 0 }
82+
)
83+
84+
// Usage in a test
85+
Task {
86+
do {
87+
let data = try await mock.fetchData()
88+
print(data) // Output: "Mocked Data"
89+
} catch {
90+
XCTFail("Unexpected error: \(error)")
91+
}
92+
}
93+
94+
let isValid = mock.processData(input: 5)
95+
XCTAssertTrue(isValid)
96+
```
97+
98+
# Limitations
99+
- **Associated Types**: The `@Mocked` macro currently supports protocols with associated types using generics. However, there may be scenarios where creating a type-erased wrapper could be beneficial, especially for protocols with complex associated type relationships.
100+
- **Protocol Inheritance**: When mocking protocols that inherit from other protocols, the `@Mocked` macro will not automatically generate parent mocks for child protocols. Instead, extend the parent protocols or the child protocol to provide the necessary values or functions to conform to the inherited requirements.
101+
102+
# Best Practices
103+
- **Define Clear Protocols**: Define small, focused protocols that capture a specific piece of functionality. This makes the generated mocks easier to use and understand.
104+
- **Avoid Over-Mocking**: Avoid mocking too much behavior in a single test, as it can lead to brittle tests that are difficult to maintain. Instead, focus on the specific interactions you want to verify.
105+
- **Use Closures Thoughtfully**: Provide closures that simulate realistic behavior to make your tests more meaningful. For example, simulate network delays with `async` closures or return specific error types to test error handling paths.
106+
*/
44107
@attached(peer, names: prefixed(Mocked))
45108
public macro Mocked() = #externalMacro(
46109
module: "MockedMacros",

Sources/MockedClient/main.swift

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,20 @@
11
import Mocked
22

3-
protocol ThisBreaksShit {
4-
var broken: String { get }
5-
}
6-
73
@Mocked
8-
protocol SomeParameter: Sendable {
9-
var title: String { get set }
10-
var description: String { get }
4+
protocol ExampleProtocol: Sendable {
5+
associatedtype ItemType: Codable
6+
associatedtype ItemValue: Equatable
117

12-
func someMethod()
13-
func someMethod(parameter: Int)
14-
func someMethod(with parameter: Int)
8+
var name: String { get set }
9+
var count: Int { get }
10+
var isEnabled: Bool { get set }
1511

16-
func someOtherMethod() throws -> String
17-
func someOtherMethod() async throws -> String
12+
func fetchItem(withID id: Int) async throws -> ItemType
13+
func saveItem(_ item: ItemType) throws -> Bool
1814

19-
func someAsyncMethod() async -> String
20-
21-
func someOptionalMethod() -> String?
15+
func processAllItems() async
16+
func reset()
17+
func optionalItem() -> ItemType?
2218
}
2319

24-
Task { @MainActor in
25-
let mockedParameter = MockedSomeParameter(
26-
title: "Hello",
27-
description: "Descrip",
28-
someMethodParameter: { print("\($0)") },
29-
someOtherMethodAsyncThrows: { "?" }
30-
)
31-
32-
mockedParameter.someMethod(parameter: 3)
33-
let value = try await mockedParameter.someOtherMethod()
34-
35-
print(value)
36-
37-
}
20+
let mock = MockedExampleProtocol<String, String>(name: "Leif", count: 0, isEnabled: true)

Sources/MockedMacros/MockedMacro.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,41 @@ public struct MockedMacro: PeerMacro {
4343
let functionVariableInitDefinitions: String = functionVariableInitDefinitions(functions: functions)
4444
let functionVariableInitAssignments: String = functionVariableInitAssignments(functions: functions)
4545
let functionImplementations: String = functionImplementations(functions: functions)
46+
47+
// Check if the protocol conforms to AnyObject
48+
let requiresClassConformance = protocolDecl.inheritanceClause?.inheritedTypes.contains(where: {
49+
$0.type.description.trimmingCharacters(in: .whitespacesAndNewlines) == "AnyObject"
50+
}) ?? false
51+
52+
let objectType: String = requiresClassConformance ? "class" : "struct"
53+
54+
// Check for associated types in the protocol
55+
var associatedTypes: [String] = []
56+
57+
for member in protocolDecl.memberBlock.members {
58+
if let associatedTypeDecl = member.decl.as(AssociatedTypeDeclSyntax.self) {
59+
let name = associatedTypeDecl.name.text
60+
let constraint = associatedTypeDecl.inheritanceClause?.description.trimmingCharacters(in: .whitespacesAndNewlines)
61+
62+
if let constraint {
63+
associatedTypes.append("\(name)\(constraint)")
64+
} else {
65+
associatedTypes.append(name)
66+
}
67+
}
68+
}
69+
70+
// Construct generic type parameters if there are associated types
71+
let genericValues = if associatedTypes.isEmpty {
72+
""
73+
} else {
74+
"<" + associatedTypes.joined(separator: ", ") + ">"
75+
}
4676

4777
return [
4878
"""
4979
/// Mocked version of \(raw: protocolDecl.name.text)
50-
struct \(raw: mockClassName): \(raw: protocolDecl.name.text) {
80+
\(raw: objectType) \(raw: mockClassName)\(raw: genericValues): \(raw: protocolDecl.name.text) {
5181
// MARK: - \(raw: mockClassName) Variables
5282
5383
\(raw: variablesDefinitions)

Tests/MockedTests/MockedTests.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ final class MockedMacroTests: XCTestCase {
171171
@Mocked
172172
protocol ExampleProtocol: Sendable {
173173
associatedtype ItemType
174+
associatedtype ItemValue: Codable
175+
associatedtype ItemKey: Hashable
174176
175177
var name: String { get set }
176178
var count: Int { get }
@@ -187,6 +189,8 @@ final class MockedMacroTests: XCTestCase {
187189
expandedSource: """
188190
protocol ExampleProtocol: Sendable {
189191
associatedtype ItemType
192+
associatedtype ItemValue: Codable
193+
associatedtype ItemKey: Hashable
190194
191195
var name: String { get set }
192196
var count: Int { get }
@@ -201,7 +205,7 @@ final class MockedMacroTests: XCTestCase {
201205
}
202206
203207
/// Mocked version of ExampleProtocol
204-
struct MockedExampleProtocol: ExampleProtocol {
208+
struct MockedExampleProtocol<ItemType, ItemValue: Codable, ItemKey: Hashable>: ExampleProtocol {
205209
// MARK: - MockedExampleProtocol Variables
206210
207211
var name: String
@@ -308,7 +312,7 @@ final class MockedMacroTests: XCTestCase {
308312
}
309313
310314
@Mocked
311-
protocol CustomProtocol: DefaultProtocol {
315+
protocol CustomProtocol: DefaultProtocol, AnyObject {
312316
func customMethod() -> Bool
313317
}
314318
""",
@@ -322,12 +326,12 @@ final class MockedMacroTests: XCTestCase {
322326
return "default"
323327
}
324328
}
325-
protocol CustomProtocol: DefaultProtocol {
329+
protocol CustomProtocol: DefaultProtocol, AnyObject {
326330
func customMethod() -> Bool
327331
}
328332
329333
/// Mocked version of CustomProtocol
330-
struct MockedCustomProtocol: CustomProtocol {
334+
class MockedCustomProtocol: CustomProtocol {
331335
// MARK: - MockedCustomProtocol Variables
332336
333337

0 commit comments

Comments
 (0)