Skip to content

Instantly share code, notes, and snippets.

@kruperfone
Last active March 20, 2026 09:35
Show Gist options
  • Select an option

  • Save kruperfone/8e95ecd7fffa71bdd23887ee058ab404 to your computer and use it in GitHub Desktop.

Select an option

Save kruperfone/8e95ecd7fffa71bdd23887ee058ab404 to your computer and use it in GitHub Desktop.
Spy Macro example
Docs
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/attributes/
https://developer.apple.com/documentation/swift/applying-macros
Examples
https://github.com/Matejkob/swift-spyable/tree/0d031775bcc90d6a39f43479d4fe55f7e6a7673f
https://github.com/krzysztofzablocki/Swift-Macros
https://github.com/apple/swift-syntax/blob/main/Examples/Sources/ExamplePlugin/Macros.swift#L92
Swift forum
https://forums.swift.org/t/macros-accessing-the-parent-context-of-a-syntax-node-passed-to-a-macro/64443/19
https://forums.swift.org/t/examples-of-extensionmacro/66717/9
Other
https://dev.to/francescoleoni98/how-to-create-swift-macros-with-xcode-15-3kll
https://blog.leonifrancesco.com/articles/swift-macros
https://www.avanderlee.com/swift/macros/
/*
@attached(peer, names: suffixed(Spy))
public macro spy() = #externalMacro(module: "SpyMacroMacros", type: "SpyMacro")
*/
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
/// Implementation of the `stringify` macro, which takes an expression
/// of any type and produces a tuple containing the value of that expression
/// and the source code that produced the value. For example
///
/// #stringify(x + y)
///
/// will expand to
///
/// (x + y, "x + y")
public struct StringifyMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
guard let argument = node.argumentList.first?.expression else {
fatalError("compiler bug: the macro does not have any arguments")
}
return "(\(argument), \(literal: argument.description))"
}
}
/**
Will add a spy next to protocol declaration.
Example:
```
@spy()
protocol MyProtocol {
var invokedParameter
var parameter: Int { get set }
func function(integer: Int, boolean: Bool) -> Double {}
}
```
will expand to
```
class MyProtocolSpy: MyProtocol {
var invokedParameterGetCount: Int = 0
var stubbedParameterGetResults: [Int] = []
var stubbedParameterGetResult: Int!
var invokedParameterSetParameters: [Int] = []
var parameter: Int {
get {
invokedParameterGetCount += 1
return stubbedParameterGetResults.removeFirst() ?? stubbedParameterGetResult
}
set {
invokedParameterSetParameters.append(newValue)
}
}
var invokedFunctionParameters: [(integer: Int, boolean: Bool)] = []
var stubbedFunctionResults: [Double] = []
var stubbedFunctionResult: Double!
func function(integer: Int, boolean: Bool) -> Double {
invokedFunctionParameters.append((integer, boolean))
return stubbedFunctionResults.removeFirst() ?? stubbedFunctionResult
}
}
```
*/
public struct SpyMacro: PeerMacro {
public static func expansion(
of node: SwiftSyntax.AttributeSyntax,
providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol,
in context: some SwiftSyntaxMacros.MacroExpansionContext
) throws -> [SwiftSyntax.DeclSyntax] {
guard let decl = declaration.as(ProtocolDeclSyntax.self) else { return [] }
return [
DeclSyntax(
ClassDeclSyntax(
name: TokenSyntax.identifier(decl.name.text + "Spy"),
inheritanceClause: InheritanceClauseSyntax {
InheritedTypeSyntax(
type: IdentifierTypeSyntax(name: decl.name)
)
},
memberBlockBuilder: {
for member in decl.memberBlock.members {
if let function = member.decl.as(FunctionDeclSyntax.self) {
handleFunction(function)
} else if let variable = member.decl.as(VariableDeclSyntax.self) {
for block in handleVariable(variable) {
block
}
}
}
}
)
)
]
}
private static func handleVariable(_ input: VariableDeclSyntax) -> [MemberBlockItemListSyntax] {
var identifiers: [IdentifierPatternSyntax] = []
var type: TypeSyntax!
var hasSetter: Bool = false
for binding in input.bindings {
if let value = binding.pattern.as(IdentifierPatternSyntax.self) {
identifiers.append(value.trimmed)
}
if let value = binding.typeAnnotation?.type.as(TypeSyntax.self)?.trimmed {
type = value
}
for accessor in binding.accessorBlock?.accessors.as(AccessorDeclListSyntax.self) ?? [] {
guard let accessor = accessor.as(AccessorDeclSyntax.self) else { continue }
switch accessor.accessorSpecifier.tokenKind {
case .keyword(.set): hasSetter = true
default: continue
}
}
}
return identifiers.map { identifier in
MemberBlockItemListSyntax {
let capitalizedName: DeclSyntax = "\(raw: identifier.identifier.text.capitalized)"
// MARK: Getter spies and stubs
"""
var invoked\(capitalizedName)GetCount: Int = 0
var stubbed\(capitalizedName)GetResults: [\(type)] = []
"""
if type.isOptional {
"var stubbed\(capitalizedName)GetResult: \(type)"
} else {
"var stubbed\(capitalizedName)GetResult: \(type)!"
}
// MARK: Setter stubs
if hasSetter {
"var invoked\(capitalizedName)SetParameters: [\(type)] = []"
}
// MARK: Variable declaration and getter
"""
var \(identifier): \(type) {
get {
invoked\(capitalizedName)GetCount += 1
return stubbed\(capitalizedName)GetResults.removeFirst() ?? stubbed\(capitalizedName)GetResult
}
"""
// MARK: Setter spy
if hasSetter {
"""
set {
invoked\(capitalizedName)SetParameters.append(newValue)
}
"""
}
"\n }"
}
.with(\.leadingTrivia, .newlines(2))
}
}
private static func handleFunction(_ input: FunctionDeclSyntax) -> MemberBlockItemListSyntax {
let capitalizedName: DeclSyntax = "\(raw: input.name.text.capitalized)"
/// name for invoked parameters
func name(for parameter: FunctionParameterListSyntax.Element) -> TokenSyntax {
(parameter.secondName ?? parameter.firstName).trimmed
}
/// Type for invoked parameters
func invokedType(for parameter: FunctionParameterListSyntax.Element) -> TypeSyntax {
if let syntax = parameter.type.as(AttributedTypeSyntax.self) {
return syntax.baseType
} else {
return parameter.type
}
}
let parameters = input.signature.parameterClause.parameters.filter {
// Filter out non-escaping closures because we can't append it to invoked parameters array
guard let parameterSyntax = $0.as(FunctionParameterSyntax.self) else { return true }
if parameterSyntax.type.is(IdentifierTypeSyntax.self) {
return true
} else if parameterSyntax.type.isOptional {
// Optional closures are always escaping
return true
} else if parameterSyntax.type.is(FunctionTypeSyntax.self) {
// Closure without @escaping
return false
} else if let type = parameterSyntax.type.as(AttributedTypeSyntax.self) {
return type.attributes
.contains {
$0.as(AttributeSyntax.self)?
.attributeName.as(IdentifierTypeSyntax.self)?
.name
.tokenKind == .identifier("escaping")
}
}
return false
}
return MemberBlockItemListSyntax {
// MARK: Invoked parameters
switch parameters.count {
case 0:
"var invoked\(capitalizedName)Parameters: [(Void, Void)] = []"
case 1:
let parameter = parameters.first!
"var invoked\(capitalizedName)Parameters: [(\(name(for: parameter)): \(invokedType(for: parameter)), Void)] = []"
default:
let params = parameters
.map { "\(name(for: $0)): \(invokedType(for: $0))" }
.joined(separator: ",")
"var invoked\(capitalizedName)Parameters: [(\(raw: params))] = []"
}
// MARK: Stubbed result
if let clause = input.signature.returnClause, clause.type.as(IdentifierTypeSyntax.self)?.name.text != "Void" {
if input.signature.effectSpecifiers?.throwsSpecifier != nil {
// Stub with Result when throws
"var stubbed\(capitalizedName)Results: [Result<\(clause.type), Error>] = []"
"var stubbed\(capitalizedName)Result: Result<\(clause.type), Error>!"
} else {
"var stubbed\(capitalizedName)Results: [\(clause.type)] = []"
if clause.type.isOptional {
// Skip unwrap modifier when already optional
"var stubbed\(capitalizedName)Result: \(clause.type)"
} else {
"var stubbed\(capitalizedName)Result: \(clause.type)!"
}
}
} else if input.signature.effectSpecifiers?.throwsSpecifier != nil {
// Stub with Result when throws but not returns
"var stubbed\(capitalizedName)Results: [Result<Void, Error>] = []"
"var stubbed\(capitalizedName)Result: Result<Void, Error>!"
}
// MARK: Function declaration
FunctionDeclSyntax(
name: input.name,
signature: input.signature
) {
// MARK: Fill invoked
switch parameters.count {
case 0:
"invoked\(capitalizedName)Parameters.append(((), ()))"
case 1:
let parameter = parameters.first!
"invoked\(capitalizedName)Parameters.append((\(name(for: parameter)), ()))"
default:
let params = parameters
.map { "\(name(for: $0))" }
.joined(separator: ",")
"invoked\(capitalizedName)Parameters.append((\(raw: params)))"
}
// MARK: Return
if let clause = input.signature.returnClause, clause.type.as(IdentifierTypeSyntax.self)?.name.text != "Void" {
if input.signature.effectSpecifiers?.throwsSpecifier != nil {
// try to get result if throws
"return try (stubbed\(capitalizedName)Results.removeFirst() ?? stubbed\(capitalizedName)Result).get()"
} else {
"return stubbed\(capitalizedName)Results.removeFirst() ?? stubbed\(capitalizedName)Result"
}
} else if input.signature.effectSpecifiers?.throwsSpecifier != nil {
// try to get result if throws
"return try (stubbed\(capitalizedName)Results.removeFirst() ?? stubbed\(capitalizedName)Result).get()"
}
}
}
.with(\.leadingTrivia, .newlines(2))
}
}
private extension TypeSyntax {
var isOptional: Bool {
self.is(OptionalTypeSyntax.self) || self.is(ImplicitlyUnwrappedOptionalTypeSyntax.self)
}
}
@main
struct SpyMacroPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
StringifyMacro.self,
SpyMacro.self
]
}
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
// Macro implementations build for the host, so the corresponding module is not available when cross-compiling. Cross-compiled tests may still make use of the macro itself in end-to-end tests.
import SpyMacroMacros
let testMacros: [String: Macro.Type] = [
"stringify": StringifyMacro.self,
"spy": SpyMacro.self
]
final class SpyMacroTests: XCTestCase {
// MARK: - Variables
func test_variable_whenHasGetter() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
var foobar: Type { get }
}
""",
expandedSource: """
protocol TestProtocol {
var foobar: Type { get }
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarGetCount: Int = 0
var stubbedFoobarGetResults: [Type] = []
var stubbedFoobarGetResult: Type!
var foobar: Type {
get {
invokedFoobarGetCount += 1
return stubbedFoobarGetResults.removeFirst() ?? stubbedFoobarGetResult
}
}
}
""",
macros: testMacros
)
}
func test_variable_whenHasGetter_andSetter() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
var foobar: Type { get set }
}
""",
expandedSource: """
protocol TestProtocol {
var foobar: Type { get set }
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarGetCount: Int = 0
var stubbedFoobarGetResults: [Type] = []
var stubbedFoobarGetResult: Type!
var invokedFoobarSetParameters: [Type] = []
var foobar: Type {
get {
invokedFoobarGetCount += 1
return stubbedFoobarGetResults.removeFirst() ?? stubbedFoobarGetResult
}
set {
invokedFoobarSetParameters.append(newValue)
}
}
}
""",
macros: testMacros
)
}
func test_variable_whenHasGetter_andSetter_andOptional() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
var foobar: Type? { get set }
}
""",
expandedSource: """
protocol TestProtocol {
var foobar: Type? { get set }
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarGetCount: Int = 0
var stubbedFoobarGetResults: [Type?] = []
var stubbedFoobarGetResult: Type?
var invokedFoobarSetParameters: [Type?] = []
var foobar: Type? {
get {
invokedFoobarGetCount += 1
return stubbedFoobarGetResults.removeFirst() ?? stubbedFoobarGetResult
}
set {
invokedFoobarSetParameters.append(newValue)
}
}
}
""",
macros: testMacros
)
}
func test_variable_whenHasGetter_andSetter_andOptionalForced() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
var foobar: Type! { get set }
}
""",
expandedSource: """
protocol TestProtocol {
var foobar: Type! { get set }
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarGetCount: Int = 0
var stubbedFoobarGetResults: [Type!] = []
var stubbedFoobarGetResult: Type!
var invokedFoobarSetParameters: [Type!] = []
var foobar: Type! {
get {
invokedFoobarGetCount += 1
return stubbedFoobarGetResults.removeFirst() ?? stubbedFoobarGetResult
}
set {
invokedFoobarSetParameters.append(newValue)
}
}
}
""",
macros: testMacros
)
}
func test_variable_whenList() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
var foobar1, foobar2: Type { get }
}
""",
expandedSource: """
protocol TestProtocol {
var foobar1, foobar2: Type { get }
}
class TestProtocolSpy: TestProtocol {
var invokedFoobar1GetCount: Int = 0
var stubbedFoobar1GetResults: [Type] = []
var stubbedFoobar1GetResult: Type!
var foobar1: Type {
get {
invokedFoobar1GetCount += 1
return stubbedFoobar1GetResults.removeFirst() ?? stubbedFoobar1GetResult
}
}
var invokedFoobar2GetCount: Int = 0
var stubbedFoobar2GetResults: [Type] = []
var stubbedFoobar2GetResult: Type!
var foobar2: Type {
get {
invokedFoobar2GetCount += 1
return stubbedFoobar2GetResults.removeFirst() ?? stubbedFoobar2GetResult
}
}
}
""",
macros: testMacros
)
}
// MARK: - Functions
// MARK: Plain
func test_function_whenHasNoArgumentsAndNoReturn() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar()
}
""",
expandedSource: """
protocol TestProtocol {
func foobar()
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(Void, Void)] = []
func foobar() {
invokedFoobarParameters.append(((), ()))
}
}
""",
macros: testMacros
)
}
// MARK: Returns
func test_function_whenReturnsVoid() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar() -> Void
}
""",
expandedSource: """
protocol TestProtocol {
func foobar() -> Void
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(Void, Void)] = []
func foobar() -> Void {
invokedFoobarParameters.append(((), ()))
}
}
""",
macros: testMacros
)
}
func test_function_whenReturnsSomething() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar() -> ReturnType
}
""",
expandedSource: """
protocol TestProtocol {
func foobar() -> ReturnType
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(Void, Void)] = []
var stubbedFoobarResults: [ReturnType] = []
var stubbedFoobarResult: ReturnType!
func foobar() -> ReturnType {
invokedFoobarParameters.append(((), ()))
return stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult
}
}
""",
macros: testMacros
)
}
func test_function_whenReturnsOptional() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar() -> ReturnType?
}
""",
expandedSource: """
protocol TestProtocol {
func foobar() -> ReturnType?
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(Void, Void)] = []
var stubbedFoobarResults: [ReturnType?] = []
var stubbedFoobarResult: ReturnType?
func foobar() -> ReturnType? {
invokedFoobarParameters.append(((), ()))
return stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult
}
}
""",
macros: testMacros
)
}
func test_function_whenReturnsOptionalForced() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar() -> ReturnType!
}
""",
expandedSource: """
protocol TestProtocol {
func foobar() -> ReturnType!
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(Void, Void)] = []
var stubbedFoobarResults: [ReturnType!] = []
var stubbedFoobarResult: ReturnType!
func foobar() -> ReturnType! {
invokedFoobarParameters.append(((), ()))
return stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult
}
}
""",
macros: testMacros
)
}
func test_function_whenReturnsClosure() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar() -> ((InputType1, InputType2) -> ReturnType)
}
""",
expandedSource: """
protocol TestProtocol {
func foobar() -> ((InputType1, InputType2) -> ReturnType)
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(Void, Void)] = []
var stubbedFoobarResults: [((InputType1, InputType2) -> ReturnType)] = []
var stubbedFoobarResult: ((InputType1, InputType2) -> ReturnType)!
func foobar() -> ((InputType1, InputType2) -> ReturnType) {
invokedFoobarParameters.append(((), ()))
return stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult
}
}
""",
macros: testMacros
)
}
func test_function_whenReturnsAnyPublisher() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar() -> AnyPublisher<SuccessType, FailureType>
}
""",
expandedSource: """
protocol TestProtocol {
func foobar() -> AnyPublisher<SuccessType, FailureType>
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(Void, Void)] = []
var stubbedFoobarResults: [AnyPublisher<SuccessType, FailureType>] = []
var stubbedFoobarResult: AnyPublisher<SuccessType, FailureType>!
func foobar() -> AnyPublisher<SuccessType, FailureType> {
invokedFoobarParameters.append(((), ()))
return stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult
}
}
""",
macros: testMacros
)
}
func test_function_whenReturnsSubject() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar() -> PassthroughSubject<SuccessType, FailureType>
}
""",
expandedSource: """
protocol TestProtocol {
func foobar() -> PassthroughSubject<SuccessType, FailureType>
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(Void, Void)] = []
var stubbedFoobarResults: [PassthroughSubject<SuccessType, FailureType>] = []
var stubbedFoobarResult: PassthroughSubject<SuccessType, FailureType>!
func foobar() -> PassthroughSubject<SuccessType, FailureType> {
invokedFoobarParameters.append(((), ()))
return stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult
}
}
""",
macros: testMacros
)
}
// MARK: Arguments
func test_function_whenHasArgument() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar(arg: ArgType)
}
""",
expandedSource: """
protocol TestProtocol {
func foobar(arg: ArgType)
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(arg: ArgType, Void)] = []
func foobar(arg: ArgType) {
invokedFoobarParameters.append((arg, ()))
}
}
""",
macros: testMacros
)
}
func test_function_whenHasOptionalArgument() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar(arg: ArgType?)
}
""",
expandedSource: """
protocol TestProtocol {
func foobar(arg: ArgType?)
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(arg: ArgType?, Void)] = []
func foobar(arg: ArgType?) {
invokedFoobarParameters.append((arg, ()))
}
}
""",
macros: testMacros
)
}
func test_function_whenHasOptionalForcedArgument() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar(arg: ArgType!)
}
""",
expandedSource: """
protocol TestProtocol {
func foobar(arg: ArgType!)
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(arg: ArgType!, Void)] = []
func foobar(arg: ArgType!) {
invokedFoobarParameters.append((arg, ()))
}
}
""",
macros: testMacros
)
}
func test_function_whenHasArgumentWithSecondName() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar(with arg: ArgType)
}
""",
expandedSource: """
protocol TestProtocol {
func foobar(with arg: ArgType)
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(arg: ArgType, Void)] = []
func foobar(with arg: ArgType) {
invokedFoobarParameters.append((arg, ()))
}
}
""",
macros: testMacros
)
}
func test_function_whenHasArguments() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar(arg1: ArgType1, arg2: ArgType2)
}
""",
expandedSource: """
protocol TestProtocol {
func foobar(arg1: ArgType1, arg2: ArgType2)
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(arg1: ArgType1, arg2: ArgType2)] = []
func foobar(arg1: ArgType1, arg2: ArgType2) {
invokedFoobarParameters.append((arg1, arg2))
}
}
""",
macros: testMacros
)
}
// Closure arguments
func test_function_whenHasEscapingClosureArgument() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar(closure: @escaping () -> Void)
}
""",
expandedSource: """
protocol TestProtocol {
func foobar(closure: @escaping () -> Void)
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(closure: () -> Void, Void)] = []
func foobar(closure: @escaping () -> Void) {
invokedFoobarParameters.append((closure, ()))
}
}
""",
macros: testMacros
)
}
func test_function_whenHasNotEscapingClosureArgument() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar(closure: () -> Void)
}
""",
expandedSource: """
protocol TestProtocol {
func foobar(closure: () -> Void)
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(Void, Void)] = []
func foobar(closure: () -> Void) {
invokedFoobarParameters.append(((), ()))
}
}
""",
macros: testMacros
)
}
func test_function_whenHasOptionalClosureArgument() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar(closure: (() -> Void)?)
}
""",
expandedSource: """
protocol TestProtocol {
func foobar(closure: (() -> Void)?)
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(closure: (() -> Void)?, Void)] = []
func foobar(closure: (() -> Void)?) {
invokedFoobarParameters.append((closure, ()))
}
}
""",
macros: testMacros
)
}
// MARK: Throws
func test_function_whenThrowsAndReturnsNothing() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar() throws
}
""",
expandedSource: """
protocol TestProtocol {
func foobar() throws
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(Void, Void)] = []
var stubbedFoobarResults: [Result<Void, Error>] = []
var stubbedFoobarResult: Result<Void, Error>!
func foobar() throws {
invokedFoobarParameters.append(((), ()))
return try (stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult).get()
}
}
""",
macros: testMacros
)
}
func test_function_whenThrowsAndReturnsSomething() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar() throws -> ReturnType
}
""",
expandedSource: """
protocol TestProtocol {
func foobar() throws -> ReturnType
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(Void, Void)] = []
var stubbedFoobarResults: [Result<ReturnType, Error>] = []
var stubbedFoobarResult: Result<ReturnType, Error>!
func foobar() throws -> ReturnType {
invokedFoobarParameters.append(((), ()))
return try (stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult).get()
}
}
""",
macros: testMacros
)
}
func test_function_whenThrowsAndReturnsOptional() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar() throws -> ReturnType?
}
""",
expandedSource: """
protocol TestProtocol {
func foobar() throws -> ReturnType?
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(Void, Void)] = []
var stubbedFoobarResults: [Result<ReturnType?, Error>] = []
var stubbedFoobarResult: Result<ReturnType?, Error>!
func foobar() throws -> ReturnType? {
invokedFoobarParameters.append(((), ()))
return try (stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult).get()
}
}
""",
macros: testMacros
)
}
func test_function_whenThrowsAndReturnsOptionalForced() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar() throws -> ReturnType!
}
""",
expandedSource: """
protocol TestProtocol {
func foobar() throws -> ReturnType!
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(Void, Void)] = []
var stubbedFoobarResults: [Result<ReturnType!, Error>] = []
var stubbedFoobarResult: Result<ReturnType!, Error>!
func foobar() throws -> ReturnType! {
invokedFoobarParameters.append(((), ()))
return try (stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult).get()
}
}
""",
macros: testMacros
)
}
// MARK: Async
func test_function_whenAsync() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar() async
}
""",
expandedSource: """
protocol TestProtocol {
func foobar() async
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(Void, Void)] = []
func foobar() async {
invokedFoobarParameters.append(((), ()))
}
}
""",
macros: testMacros
)
}
func test_function_whenAsyncThrows() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar() async throws
}
""",
expandedSource: """
protocol TestProtocol {
func foobar() async throws
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(Void, Void)] = []
var stubbedFoobarResults: [Result<Void, Error>] = []
var stubbedFoobarResult: Result<Void, Error>!
func foobar() async throws {
invokedFoobarParameters.append(((), ()))
return try (stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult).get()
}
}
""",
macros: testMacros
)
}
// MARK: Everything
func test_function_whenComplex() {
assertMacroExpansion(
"""
@spy()
protocol TestProtocol {
func foobar(
with arg1: Arg1,
arg2: Arg2?,
closure: @escaping (cArg1: CArg1, cArg2: CArg2) -> ClosureReturnType
) async throws -> ReturnType
}
""",
expandedSource: """
protocol TestProtocol {
func foobar(
with arg1: Arg1,
arg2: Arg2?,
closure: @escaping (cArg1: CArg1, cArg2: CArg2) -> ClosureReturnType
) async throws -> ReturnType
}
class TestProtocolSpy: TestProtocol {
var invokedFoobarParameters: [(arg1: Arg1, arg2: Arg2?, closure: (cArg1: CArg1, cArg2: CArg2) -> ClosureReturnType)] = []
var stubbedFoobarResults: [Result<ReturnType, Error>] = []
var stubbedFoobarResult: Result<ReturnType, Error>!
func foobar(
with arg1: Arg1,
arg2: Arg2?,
closure: @escaping (cArg1: CArg1, cArg2: CArg2) -> ClosureReturnType
) async throws -> ReturnType {
invokedFoobarParameters.append((arg1, arg2, closure))
return try (stubbedFoobarResults.removeFirst() ?? stubbedFoobarResult).get()
}
}
""",
macros: testMacros
)
}
// MARK: - Var and Func
func test_variableAndFunction() {
assertMacroExpansion(
"""
@spy()
protocol MyProtocol {
var parameter: Int { get set }
func function(integer: Int, boolean: Bool) -> Double
}
""",
expandedSource: """
protocol MyProtocol {
var parameter: Int { get set }
func function(integer: Int, boolean: Bool) -> Double
}
class MyProtocolSpy: MyProtocol {
var invokedParameterGetCount: Int = 0
var stubbedParameterGetResults: [Int] = []
var stubbedParameterGetResult: Int!
var invokedParameterSetParameters: [Int] = []
var parameter: Int {
get {
invokedParameterGetCount += 1
return stubbedParameterGetResults.removeFirst() ?? stubbedParameterGetResult
}
set {
invokedParameterSetParameters.append(newValue)
}
}
var invokedFunctionParameters: [(integer: Int, boolean: Bool)] = []
var stubbedFunctionResults: [Double] = []
var stubbedFunctionResult: Double!
func function(integer: Int, boolean: Bool) -> Double {
invokedFunctionParameters.append((integer, boolean))
return stubbedFunctionResults.removeFirst() ?? stubbedFunctionResult
}
}
""",
macros: testMacros
)
}
}
protocol TestProtocol {
var variableGetSet: Int { get set }
var variableGet: Int { get }
func function()
func functionWithArguments()
func functionWithArgumentsAndReturnValue()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment