Skip to content

Commit acdc634

Browse files
authored
Leif/access levels (#10)
1 parent 1ec968d commit acdc634

File tree

7 files changed

+406
-125
lines changed

7 files changed

+406
-125
lines changed

Package.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,14 @@ let macroTarget: Target = Target.macro(
4141

4242
let package = Package(
4343
name: "Mocked",
44-
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
44+
platforms: [
45+
.macOS(.v10_15),
46+
.iOS(.v13),
47+
.tvOS(.v13),
48+
.watchOS(.v6),
49+
.macCatalyst(.v13),
50+
.visionOS(.v1)
51+
],
4552
products: [
4653
// Products define the executables and libraries a package produces, making them visible to other packages.
4754
.library(

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ Mocked is a Swift 6 compiler macro that automatically generates mock implementat
1212

1313
- **Automatic Mock Generation**: Simply annotate your protocol with `@Mocked`, and a mock implementation will be generated.
1414
- **Supports Properties and Methods**: Generates mock versions of properties and methods, including `async` and `throws` variants.
15+
- **Access Level Control**: You can specify the access level (`open`, `public`, `package`, `internal`, `fileprivate`, `private`) for the generated mock.
1516
- **Configurable Behavior**: Easily override behavior by providing closures during initialization of the mock.
17+
- **Support for Associated Types**: The `Mocked` macro handles protocols with associated types using generics.
18+
- **Automatic Detection of Class Requirements**: If the protocol conforms to `AnyObject`, a class is generated instead of a struct, maintaining reference semantics.
1619

1720
## Installation
1821

Sources/Mocked/Mocked.swift

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
/**
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.
2+
# `Mocked` Macro Documentation
33

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.
4+
The `Mocked` macro is a powerful tool used to automatically generate a mocked implementation of a protocol, simplifying unit testing by creating mock objects without the need for cumbersome manual coding. This macro includes support for associated types and automatic detection of class requirements, providing flexibility in testing and making it ideal for developers looking to reduce boilerplate and focus on writing effective tests.
55

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.
6+
## Features
7+
8+
- **Automatic Mock Generation**: The `Mocked` macro automatically generates a mock implementation for any protocol. This saves development time and reduces the amount of boilerplate code required for creating mock objects.
9+
- **Access Level Control**: You can specify the access level (`open`, `public`, `package`, `internal`, `fileprivate`, `private`) for the generated mock, making it suitable for different testing needs and code visibility requirements.
10+
- **Closure-Based Method Overrides**: Methods and properties of the protocol can be overridden by providing closures during the initialization of the generated mock, allowing precise control over method behavior in tests.
11+
- **Support for Associated Types**: The `Mocked` macro handles protocols with associated types using generics, offering flexibility for mocking complex protocol requirements.
12+
- **Automatic Detection of Class Requirements**: If the protocol conforms to `AnyObject`, the macro generates a class instead of a struct, ensuring reference semantics are preserved where needed.
13+
- **Support for `async` and `throws` Methods**: The generated mock can handle methods marked as `async` or `throws`, allowing you to create mock behaviors for asynchronous operations or error scenarios.
14+
- **Automatic Default Property Implementations**: Properties defined in the protocol are automatically backed by straightforward storage, which can be accessed and modified as needed.
15+
16+
## Usage
17+
18+
To use the `@Mocked` macro, simply attach it to a protocol declaration. The generated mock will provide all properties and methods specified in the protocol, which can be customized through closures provided during initialization. This allows for easy setup of mock behavior for testing.
19+
20+
### Example:
821

9-
Example:
1022
```swift
1123
@Mocked
1224
protocol MyProtocol {
@@ -15,26 +27,10 @@ protocol MyProtocol {
1527
}
1628
```
1729

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.
30+
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 extensive boilerplate code.
1931

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.
32+
### Example of Generated Code:
3533

36-
# Example of Generated Code
37-
For the protocol `MyProtocol`, the generated mock implementation would look like this:
3834
```swift
3935
struct MockedMyProtocol: MyProtocol {
4036
// Properties defined by the protocol
@@ -59,53 +55,65 @@ struct MockedMyProtocol: MyProtocol {
5955
}
6056
```
6157

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.
58+
## Access Level Control
59+
60+
The `@Mocked` macro allows specifying an access level for the generated mock. This can be useful when defining the visibility of mocks in your test suite or modules. The following access levels are supported:
61+
62+
- `open`: The most permissive access level, allowing subclassing and usage in other modules.
63+
- `public`: Allows usage by other modules but restricts subclassing to within the defining module.
64+
- `package`: Limits access to declarations within the same package, suitable for managing code visibility within related modules.
65+
- `internal`: The default access level in Swift, exposing the mock only within the same module.
66+
- `fileprivate`: Restricts access to the file where the mock is defined.
67+
- `private`: Restricts access to the enclosing declaration, providing the highest level of encapsulation.
68+
69+
To specify an access level, provide it as a parameter to the macro:
70+
71+
```swift
72+
@Mocked(.public)
73+
protocol MyProtocol {
74+
// Protocol requirements
75+
}
76+
```
77+
78+
## Edge Cases and Warnings
79+
80+
- **Non-Protocol Usage**: The `@Mocked` macro can only be applied to protocol definitions. Applying it to other types, such as classes or structs, will result in a compilation error.
81+
- **Unimplemented Methods**: Any method that is not explicitly overridden will call `fatalError()` when invoked, ensuring that developers are alerted if a method is called without being properly mocked. Always ensure that all necessary methods are mocked to avoid runtime crashes.
82+
- **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.
83+
84+
## Best Practices
85+
86+
- **Keep Protocols Small and Focused**: Define small, focused protocols that capture a specific piece of functionality. This makes the generated mocks easier to use and understand.
87+
- **Avoid Over-Mocking**: Mock only the behavior required for the test. Over-mocking can lead to brittle tests that are difficult to maintain.
88+
- **Use Closures Thoughtfully**: When providing closures to override protocol methods, simulate realistic behaviors to create meaningful tests. For example, introduce delays for `async` methods or specific error types for `throws` methods.
6589

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.
90+
## Advanced Usage
91+
92+
The `Mocked` macro can handle more complex protocols, including those with associated types, `async` methods, `throws` methods, or combinations of these. This makes it easy to test scenarios involving asynchronous operations, error handling, or protocols with type constraints.
6893

6994
```swift
7095
@Mocked
7196
protocol ComplexProtocol {
7297
associatedtype ItemType
73-
associatedtype ItemValue: Codable
7498
func fetchData() async throws -> ItemType
7599
func processData(input: Int) -> Bool
76-
func storeValue(value: ItemValue) -> Void
77100
}
78101

79-
let mock = MockedComplexProtocol<String, Int>(
102+
let mock = MockedComplexProtocol<String>(
80103
fetchData: { return "Mocked Data" },
81104
processData: { input in return input > 0 }
82105
)
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)
96106
```
97107

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.
108+
In this example, `MockedComplexProtocol` provides custom behavior for `fetchData` and `processData`, allowing precise control over how the protocol's methods behave in your tests.
109+
110+
## Limitations
101111

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.
112+
- **Associated Types**: While the `Mocked` macro supports protocols with associated types using generics, there may be scenarios where creating a type-erased wrapper could be beneficial, especially for protocols with complex relationships between associated types.
113+
- **Protocol Inheritance**: The `Mocked` macro will not automatically generate parent mocks for child protocols that inherit other protocols. Developers need to extend the generated mock to meet the requirements of inherited protocols manually.
106114
*/
107115
@attached(peer, names: prefixed(Mocked))
108-
public macro Mocked() = #externalMacro(
116+
public macro Mocked(_ accessLevel: AccessLevel = .internal) = #externalMacro(
109117
module: "MockedMacros",
110118
type: "MockedMacro"
111119
)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
Represents different levels of access control for declarations in Swift.
3+
4+
Access levels determine which parts of your code can access a declaration.
5+
Swift provides several levels of access: `open`, `public`, `package`, `internal`, `fileprivate`, and `private`.
6+
Each level restricts or permits access in increasing order of specificity.
7+
8+
- `open`: The most permissive access level, allowing a declaration to be used and subclassed
9+
within the module it’s defined in and by other modules that import it. Use `open` when you
10+
want code outside the module to subclass or override entities.
11+
12+
- `public`: Allows a declaration to be used by other modules but restricts subclassing
13+
and overriding to within the defining module. Use `public` when exposing functionality
14+
that should not be subclassed or overridden by external modules.
15+
16+
- `package`: Limits access to declarations within the same package, making them inaccessible
17+
outside the package's scope. This is ideal for managing code visibility within related
18+
modules that are distributed together.
19+
20+
- `internal`: Default access level in Swift. Declarations are accessible within the same
21+
module but are hidden from external modules. This is ideal for exposing code internally
22+
without making it available to other modules.
23+
24+
- `private`: Restricts access to the enclosing declaration (such as a type or extension).
25+
Declarations marked as `private` are inaccessible outside the enclosing scope, providing
26+
the highest level of encapsulation.
27+
28+
- `fileprivate`: Allows access within the same file only. Unlike `private`, `fileprivate`
29+
allows access to declarations across types and extensions within a single file, providing
30+
a middle ground between `internal` and `private`.
31+
*/
32+
public enum AccessLevel: String {
33+
case `open`
34+
case `public`
35+
case `package`
36+
case `internal`
37+
case `private`
38+
case `fileprivate`
39+
}

Sources/MockedClient/main.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Mocked
22

3-
@Mocked
3+
@Mocked(.fileprivate)
44
protocol ExampleProtocol: Sendable {
55
associatedtype ItemType: Codable
66
associatedtype ItemValue: Equatable
@@ -16,5 +16,3 @@ protocol ExampleProtocol: Sendable {
1616
func reset()
1717
func optionalItem() -> ItemType?
1818
}
19-
20-
let mock = MockedExampleProtocol<String, String>(name: "Leif", count: 0, isEnabled: true)

0 commit comments

Comments
 (0)