├── .github └── workflows │ ├── doc.yml │ └── pr.yml ├── .gitignore ├── .swiftlint.yml ├── LICENSE ├── Package.swift ├── README.md ├── Scripts ├── doc.sh ├── open.sh ├── test.sh └── utils.sh ├── Sources ├── Mockable │ ├── Builder │ │ ├── Builder.swift │ │ ├── FunctionBuilders │ │ │ ├── FunctionActionBuilder.swift │ │ │ ├── FunctionReturnBuilder.swift │ │ │ ├── FunctionVerifyBuilder.swift │ │ │ ├── ThrowingFunctionActionBuilder.swift │ │ │ ├── ThrowingFunctionReturnBuilder.swift │ │ │ └── ThrowingFunctionVerifyBuilder.swift │ │ ├── MockableService.swift │ │ └── PropertyBuilders │ │ │ ├── PropertyActionBuilder.swift │ │ │ ├── PropertyReturnBuilder.swift │ │ │ └── PropertyVerifyBuilder.swift │ ├── Documentation │ │ └── Documentation.docc │ │ │ ├── Configuration.md │ │ │ ├── Installation.md │ │ │ ├── Mockable.md │ │ │ └── Usage.md │ ├── Helpers │ │ ├── Async+Timeout.swift │ │ ├── AsyncSubject.swift │ │ └── LockedValue.swift │ ├── Macro │ │ └── MockableMacro.swift │ ├── Matcher │ │ └── Matcher.swift │ ├── Mocker │ │ ├── CaseIdentifiable.swift │ │ ├── Matchable.swift │ │ ├── MemberAction.swift │ │ ├── MemberReturn.swift │ │ ├── Mocked.swift │ │ ├── Mocker.swift │ │ ├── MockerFallback.swift │ │ ├── MockerPolicy.swift │ │ └── MockerScope.swift │ ├── Models │ │ ├── Count.swift │ │ ├── GenericValue.swift │ │ ├── Parameter+Match.swift │ │ ├── Parameter.swift │ │ ├── ReturnValue.swift │ │ └── TimeoutDuration.swift │ └── Utils │ │ └── Utils.swift └── MockableMacro │ ├── Extensions │ ├── AttributeSyntax+Extensions.swift │ ├── DeclModifierListSyntax+Extensions.swift │ ├── FunctionDeclSyntax+Extensions.swift │ ├── FunctionParameterSyntax+Extensions.swift │ ├── ProtocolDeclSyntax+Extensions.swift │ ├── String+Extensions.swift │ ├── TokenSyntax+Extensions.swift │ └── VariableDeclSyntax+Extensions.swift │ ├── Factory │ ├── Buildable │ │ ├── Buildable.swift │ │ ├── Function+Buildable.swift │ │ └── Variable+Buildable.swift │ ├── BuilderFactory.swift │ ├── Caseable │ │ ├── Caseable.swift │ │ ├── Function+Caseable.swift │ │ └── Variable+Caseable.swift │ ├── ConformanceFactory.swift │ ├── EnumFactory.swift │ ├── Factory.swift │ ├── MemberFactory.swift │ ├── MockFactory.swift │ └── Mockable │ │ ├── Function+Mockable.swift │ │ ├── Initializer+Mockable.swift │ │ ├── Mockable.swift │ │ └── Variable+Mockable.swift │ ├── MockableMacro.swift │ ├── MockableMacroError.swift │ ├── MockableMacroWarning.swift │ ├── Plugin.swift │ ├── Requirements │ ├── FunctionRequirement.swift │ ├── InitializerRequirement.swift │ ├── Requirements.swift │ └── VariableRequirement.swift │ └── Utils │ ├── Availability.swift │ ├── Messages.swift │ ├── Namespace.swift │ └── TokenFinder.swift └── Tests ├── MockableMacroTests ├── AccessModifierTests.swift ├── ActorConformanceTests.swift ├── AssociatedTypeTests.swift ├── AttributesTests.swift ├── DocCommentsTests.swift ├── ExoticParameterTests.swift ├── FunctionEffectTests.swift ├── GenericFunctionTests.swift ├── InheritedTypeMappingTests.swift ├── InitRequirementTests.swift ├── NameCollisionTests.swift ├── PropertyRequirementTests.swift ├── TypedThrowsTests_Swift6.swift └── Utils │ └── MockableMacroTestCase.swift └── MockableTests ├── ActionTests.swift ├── BuildTests.swift ├── GivenTests.swift ├── GivenTests_Swift6.swift ├── Helpers └── Task+Sleep.swift ├── PolicyTests.swift ├── Protocols ├── Models │ ├── Product.swift │ ├── User.swift │ └── UserError.swift ├── TestService.swift └── TestService_Swift6.swift └── VerifyTests.swift /.github/workflows/doc.yml: -------------------------------------------------------------------------------- 1 | name: update-documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_docs: 10 | name: Update Documentation 11 | runs-on: macOS-13 12 | env: 13 | MOCKABLE_DOC: true 14 | steps: 15 | - name: Checkout main branch 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Checkout documentation branch 21 | run: | 22 | git checkout -b documentation 23 | 24 | - name: Update documentation 25 | run: | 26 | Scripts/doc.sh 27 | git config user.name github-actions 28 | git config user.email github-actions@github.com 29 | git add docs 30 | git commit -m 'chore: update documentation' 31 | git push origin documentation --force -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: pull-request-validation 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "*" 7 | 8 | jobs: 9 | lint: 10 | name: Lint Commit Messages 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Install Node.js and NPM 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: "14" 22 | 23 | - name: Install commitlint 24 | run: npm install -g @commitlint/cli@11.0.0 @commitlint/config-conventional@11.0.0 25 | 26 | - name: Run commitlint 27 | run: commitlint -x @commitlint/config-conventional --from=${{ github.event.pull_request.base.sha }} --to=${{ github.event.pull_request.head.sha }} 28 | 29 | test: 30 | name: Build and Test using Swift 5.9 31 | runs-on: macos-14 32 | env: 33 | MOCKABLE_TEST: true 34 | steps: 35 | - name: Checkout code 36 | uses: actions/checkout@v4 37 | 38 | - name: Select Xcode 15.2 (Swift 5.9.2) 39 | run: | 40 | sudo xcode-select -s /Applications/Xcode_15.2.app 41 | swift -version 42 | 43 | - name: Run Tests 44 | run: | 45 | Scripts/test.sh 46 | 47 | test-swift6: 48 | name: Build and Test using Swift 6 49 | runs-on: macos-14 50 | env: 51 | MOCKABLE_LINT: true 52 | MOCKABLE_TEST: true 53 | steps: 54 | - name: Checkout code 55 | uses: actions/checkout@v4 56 | 57 | - name: Select Xcode 16 (Swift 6.0) 58 | run: | 59 | sudo xcode-select -s /Applications/Xcode_16.2.app 60 | swift --version 61 | 62 | - name: Run Tests 63 | run: | 64 | Scripts/lint.sh & Scripts/test.sh 65 | 66 | test-linux: 67 | name: Build and Test on Linux 68 | runs-on: ubuntu-latest 69 | strategy: 70 | matrix: 71 | swift: ["6"] # ["5", "6"] 72 | env: 73 | MOCKABLE_TEST: true 74 | steps: 75 | - name: Checkout code 76 | uses: actions/checkout@v4 77 | - uses: swift-actions/setup-swift@v2 78 | with: 79 | swift-version: ${{ matrix.swift }} 80 | - name: Run Tests 81 | run: | 82 | Scripts/test.sh 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | Package.resolved 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm 8 | .netrc 9 | .env 10 | .build -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - dynamic_inline 3 | - private_unit_test 4 | - type_body_length 5 | - valid_ibinspectable 6 | - function_default_parameter_at_end 7 | - nesting 8 | - switch_case_alignment 9 | - void_function_in_ternary 10 | - type_name 11 | 12 | opt_in_rules: 13 | - closure_spacing 14 | - convenience_type 15 | - discouraged_object_literal 16 | - empty_string 17 | - fallthrough 18 | - fatal_error_message 19 | - first_where 20 | - multiline_arguments 21 | - multiline_parameters 22 | - overridden_super_call 23 | - override_in_extension 24 | - required_enum_case 25 | - vertical_parameter_alignment_on_call 26 | - yoda_condition 27 | - array_init 28 | - explicit_init 29 | - function_default_parameter_at_end 30 | - redundant_nil_coalescing 31 | - closure_body_length 32 | - collection_alignment 33 | - conditional_returns_on_newline 34 | - contains_over_filter_count 35 | - contains_over_filter_is_empty 36 | - contains_over_first_not_nil 37 | - contains_over_range_nil_comparison 38 | - empty_collection_literal 39 | - enum_case_associated_values_count 40 | - file_name_no_space 41 | - flatmap_over_map_reduce 42 | - identical_operands 43 | - joined_default_parameter 44 | - last_where 45 | - literal_expression_end_indentation 46 | - no_extension_access_modifier 47 | - nslocalizedstring_key 48 | - operator_usage_whitespace 49 | - private_action 50 | - private_outlet 51 | - prohibited_super_call 52 | - reduce_into 53 | - redundant_type_annotation 54 | - sorted_first_last 55 | - static_operator 56 | - toggle_bool 57 | - unavailable_function 58 | - unneeded_parentheses_in_closure_argument 59 | - vertical_whitespace_closing_braces 60 | - closure_end_indentation 61 | - lower_acl_than_parent 62 | - force_unwrapping 63 | - weak_delegate 64 | analyzer_rules: 65 | - unused_declaration 66 | - unused_import 67 | 68 | 69 | closing_brace: 70 | severity: error 71 | closure_body_length: 72 | warning: 30 73 | error: 40 74 | closure_end_indentation: 75 | severity: error 76 | colon: 77 | severity: error 78 | flexible_right_spacing: false 79 | apply_to_dictionaries: true 80 | comma: 81 | severity: error 82 | conditional_returns_on_newline: 83 | severity: error 84 | if_only: true 85 | control_statement: 86 | severity: error 87 | cyclomatic_complexity: 88 | warning: 8 89 | error: 9 90 | ignores_case_statements: true 91 | empty_parameters: 92 | severity: error 93 | explicit_init: 94 | severity: error 95 | file_length: 96 | ignore_comment_only_lines: true 97 | warning: 400 98 | error: 500 99 | force_cast: 100 | severity: error 101 | force_try: 102 | severity: error 103 | force_unwrapping: 104 | severity: error 105 | function_parameter_count: 106 | warning: 5 107 | error: 6 108 | implicit_getter: 109 | severity: error 110 | large_tuple: 111 | warning: 3 112 | error: 5 113 | leading_whitespace: 114 | severity: error 115 | legacy_cggeometry_functions: 116 | severity: error 117 | legacy_constant: 118 | severity: error 119 | legacy_constructor: 120 | severity: error 121 | legacy_nsgeometry_functions: 122 | severity: error 123 | line_length: 124 | ignores_comments: true 125 | warning: 120 126 | error: 120 127 | mark: 128 | severity: error 129 | opening_brace: 130 | severity: error 131 | operator_usage_whitespace: 132 | severity: error 133 | operator_whitespace: 134 | severity: error 135 | overridden_super_call: 136 | severity: error 137 | redundant_nil_coalescing: 138 | severity: error 139 | redundant_optional_initialization: 140 | severity: error 141 | redundant_string_enum_value: 142 | severity: error 143 | redundant_void_return: 144 | severity: error 145 | return_arrow_whitespace: 146 | severity: error 147 | syntactic_sugar: 148 | severity: error 149 | todo: 150 | severity: warning 151 | trailing_comma: 152 | severity: error 153 | trailing_newline: 154 | severity: error 155 | trailing_semicolon: 156 | severity: error 157 | trailing_whitespace: 158 | severity: warning 159 | unused_closure_parameter: 160 | severity: error 161 | identifier_name: 162 | excluded: 163 | - to 164 | - id 165 | allowed_symbols: 166 | - _ 167 | vertical_parameter_alignment: 168 | severity: error 169 | vertical_whitespace: 170 | severity: error 171 | void_return: 172 | severity: error 173 | weak_delegate: 174 | severity: error 175 | lower_acl_than_parent: 176 | severity: error 177 | first_where: 178 | severity: error 179 | private_action: 180 | severity: error 181 | private_outlet: 182 | severity: error 183 | closure_spacing: 184 | severity: error 185 | array_init: 186 | severity: error 187 | statement_position: 188 | severity: error 189 | unused_enumerated: 190 | severity: error 191 | vertical_parameter_alignment_on_call: 192 | severity: error 193 | empty_string: 194 | severity: error 195 | unneeded_break_in_switch: 196 | severity: error 197 | fatal_error_message: 198 | severity: error 199 | empty_parentheses_with_trailing_closure: 200 | severity: error 201 | contains_over_first_not_nil: 202 | severity: error 203 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Kolos Foltanyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | import CompilerPluginSupport 5 | 6 | let test = Context.environment["MOCKABLE_TEST"].flatMap(Bool.init) ?? false 7 | let lint = Context.environment["MOCKABLE_LINT"].flatMap(Bool.init) ?? false 8 | let doc = Context.environment["MOCKABLE_DOC"].flatMap(Bool.init) ?? false 9 | 10 | func when(_ condition: Bool, _ list: [T]) -> [T] { condition ? list : [] } 11 | 12 | let devDependencies: [Package.Dependency] = when(test, [ 13 | .package(url: "https://github.com/pointfreeco/swift-macro-testing", exact: "0.5.2") 14 | ]) + when(lint, [ 15 | .package(url: "https://github.com/realm/SwiftLint", exact: "0.57.1"), 16 | ]) + when(doc, [ 17 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0") 18 | ]) 19 | 20 | let devPlugins: [Target.PluginUsage] = when(lint, [ 21 | .plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint") 22 | ]) 23 | 24 | let devTargets: [Target] = when(test, [ 25 | .testTarget( 26 | name: "MockableTests", 27 | dependencies: ["Mockable"], 28 | swiftSettings: [ 29 | .define("MOCKING"), 30 | .enableExperimentalFeature("StrictConcurrency"), 31 | .enableUpcomingFeature("ExistentialAny") 32 | ], 33 | plugins: devPlugins 34 | ), 35 | .testTarget( 36 | name: "MockableMacroTests", 37 | dependencies: [ 38 | "MockableMacro", 39 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 40 | .product(name: "MacroTesting", package: "swift-macro-testing"), 41 | ], 42 | swiftSettings: [.define("MOCKING")] 43 | ) 44 | ]) 45 | 46 | let package = Package( 47 | name: "Mockable", 48 | platforms: [.macOS(.v12), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], 49 | products: [ 50 | .library( 51 | name: "Mockable", 52 | targets: ["Mockable"] 53 | ) 54 | ], 55 | dependencies: devDependencies + [ 56 | .package(url: "https://github.com/swiftlang/swift-syntax.git", "509.0.0"..<"601.0.0"), 57 | .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", .upToNextMajor(from: "1.4.1")) 58 | ], 59 | targets: devTargets + [ 60 | .target( 61 | name: "Mockable", 62 | dependencies: [ 63 | "MockableMacro", 64 | .product(name: "IssueReporting", package: "xctest-dynamic-overlay") 65 | ], 66 | swiftSettings: [ 67 | .enableExperimentalFeature("StrictConcurrency"), 68 | .enableUpcomingFeature("ExistentialAny") 69 | ], 70 | plugins: devPlugins 71 | ), 72 | .macro( 73 | name: "MockableMacro", 74 | dependencies: [ 75 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 76 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 77 | ], 78 | swiftSettings: [ 79 | .enableExperimentalFeature("StrictConcurrency"), 80 | .enableUpcomingFeature("ExistentialAny") 81 | ], 82 | plugins: devPlugins 83 | ) 84 | ], 85 | swiftLanguageVersions: [.v5, .version("6")] 86 | ) 87 | -------------------------------------------------------------------------------- /Scripts/doc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | swift package \ 4 | --allow-writing-to-directory ./docs \ 5 | generate-documentation \ 6 | --target Mockable \ 7 | --output-path ./docs \ 8 | --transform-for-static-hosting \ 9 | --hosting-base-path Mockable -------------------------------------------------------------------------------- /Scripts/open.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export MOCKABLE_LINT=true 4 | export MOCKABLE_TEST=true 5 | open Package.swift 6 | -------------------------------------------------------------------------------- /Scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | ROOT_DIR="$(dirname "$SCRIPT_DIR")" 7 | source $ROOT_DIR/Scripts/utils.sh 8 | 9 | # When --vm is passed, the build and test commands are executed in a virtualized container. 10 | if [[ " $* " == *" --vm "* ]]; then 11 | CONTAINER_RUNTIME=$(get_container_runtime) 12 | $CONTAINER_RUNTIME run --rm \ 13 | --volume "$ROOT_DIR:/package" \ 14 | --workdir "/package" \ 15 | -e MOCKABLE_TEST=$MOCKABLE_TEST \ 16 | swiftlang/swift:nightly-6.1-focal \ 17 | /bin/bash -c \ 18 | "swift build --build-path ./.build/linux" 19 | else 20 | swift build --package-path "$ROOT_DIR" 21 | swift test --package-path "$ROOT_DIR" 22 | fi 23 | -------------------------------------------------------------------------------- /Scripts/utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to determine the container runtime 4 | get_container_runtime() { 5 | if command -v podman &> /dev/null; then 6 | echo "podman" 7 | elif command -v docker &> /dev/null; then 8 | echo "docker" 9 | else 10 | echo "Neither podman nor docker is installed. Please install one of them to proceed." >&2 11 | exit 1 12 | fi 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Mockable/Builder/Builder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Builder.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 21.. 6 | // 7 | 8 | /// Used to specify members of a protocol when building 9 | /// given or when clauses of a mock service. 10 | public protocol Builder { 11 | 12 | /// The mock service associated with the Builder. 13 | associatedtype Service: MockableService 14 | 15 | /// Initializes a new instance of the builder with the provided `Mocker`. 16 | /// 17 | /// - Parameter mocker: The associated service's `Mocker` to provide for subsequent builders. 18 | init(mocker: Mocker) 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Mockable/Builder/FunctionBuilders/FunctionActionBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FunctionActionBuilder.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 22.. 6 | // 7 | 8 | /// A builder for specifying actions to be performed when mocking a function. 9 | /// 10 | /// This builder is used within the context of a higher-level builder (e.g., an `ActionBuilder`) 11 | /// to specify a desired action to perform when a particular function of a mock service is called. 12 | public struct FunctionActionBuilder> { 13 | 14 | /// Convenient type for the associated service's Member. 15 | public typealias Member = T.Member 16 | 17 | /// The member being mocked. 18 | private var member: Member 19 | 20 | /// The associated service's mocker. 21 | private var mocker: Mocker 22 | 23 | /// Initializes a new instance of `FunctionActionBuilder`. 24 | /// 25 | /// - Parameters: 26 | /// - mocker: The `Mocker` instance of the associated mock service. 27 | /// - kind: The member being mocked. 28 | public init(_ mocker: Mocker, kind member: Member) { 29 | self.member = member 30 | self.mocker = mocker 31 | } 32 | 33 | /// Registers an action to be performed when the provided member is called. 34 | /// 35 | /// - Parameter action: The closure representing the action to be performed. 36 | /// - Returns: The parent builder, used for chaining additional specifications. 37 | @discardableResult 38 | public func perform(_ action: @escaping () -> Void) -> ParentBuilder { 39 | mocker.addAction(action, for: member) 40 | return .init(mocker: mocker) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Mockable/Builder/FunctionBuilders/FunctionReturnBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FunctionReturnBuilder.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 14.. 6 | // 7 | 8 | /// A builder for specifying return values or producers when mocking a function. 9 | /// 10 | /// This builder is typically used within the context of a higher-level builder (e.g., a `ReturnBuilder`) 11 | /// to specify the desired return value or a return value producer for a particular function of a mock service. 12 | public struct FunctionReturnBuilder, ReturnType, ProduceType> { 13 | 14 | /// Convenient type for the associated service's Member. 15 | public typealias Member = T.Member 16 | 17 | /// The member being mocked. 18 | private var member: Member 19 | 20 | /// The associated service's mocker. 21 | private var mocker: Mocker 22 | 23 | /// Initializes a new instance of `FunctionReturnBuilder`. 24 | /// 25 | /// - Parameters: 26 | /// - mocker: The `Mocker` instance of the associated mock service. 27 | /// - kind: The member being mocked. 28 | public init(_ mocker: Mocker, kind member: Member) { 29 | self.member = member 30 | self.mocker = mocker 31 | } 32 | 33 | /// Registers a return value to provide when the mocked member is called. 34 | /// 35 | /// - Parameter value: The return value to use for mocking the specified member. 36 | /// - Returns: The parent builder, used for chaining additional specifications. 37 | @discardableResult 38 | public func willReturn(_ value: ReturnType) -> ParentBuilder { 39 | mocker.addReturnValue(.return(value), for: member) 40 | return .init(mocker: mocker) 41 | } 42 | 43 | /// Registers a return value producing closure to use when the mocked member is called. 44 | /// 45 | /// - Parameter producer: A closure that produces a return value for the mocked member. 46 | /// - Returns: The parent builder, used for chaining additional specifications. 47 | @discardableResult 48 | public func willProduce(_ producer: ProduceType) -> ParentBuilder { 49 | mocker.addReturnValue(.produce(producer), for: member) 50 | return .init(mocker: mocker) 51 | } 52 | } 53 | 54 | extension FunctionReturnBuilder where ReturnType == Void { 55 | /// Specifies that the void function will return normally when the mocked member is called. 56 | @discardableResult 57 | public func willReturn() -> ParentBuilder { 58 | mocker.addReturnValue(.return(()), for: member) 59 | return .init(mocker: mocker) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/Mockable/Builder/FunctionBuilders/FunctionVerifyBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FunctionVerifyBuilder.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 22.. 6 | // 7 | 8 | /// A builder for verifying the number of times a mocked member was called. 9 | /// 10 | /// This builder is typically used within the context of a higher-level builder (e.g., a `VerifyBuilder`) 11 | /// to verify the expected number of invocations for a particular function of a mock service. 12 | public struct FunctionVerifyBuilder> { 13 | 14 | /// Convenient type for the associated service's Member. 15 | public typealias Member = T.Member 16 | 17 | /// The associated service's mocker. 18 | private var mocker: Mocker 19 | 20 | /// The member being verified. 21 | private var member: Member 22 | 23 | /// Initializes a new instance of `FunctionVerifyBuilder`. 24 | /// 25 | /// - Parameters: 26 | /// - mocker: The `Mocker` instance of the associated mock service. 27 | /// - kind: The member being verified. 28 | public init(_ mocker: Mocker, kind member: Member) { 29 | self.member = member 30 | self.mocker = mocker 31 | } 32 | 33 | /// Asserts the number of invocations of the specified member using `count`. 34 | /// 35 | /// - Parameter count: Specifies the expected invocation count. 36 | /// - Returns: The parent builder, used for chaining additional specifications. 37 | @discardableResult 38 | public func called( 39 | _ count: Count, 40 | fileID: StaticString = #fileID, 41 | filePath: StaticString = #filePath, 42 | line: UInt = #line, 43 | column: UInt = #column) -> ParentBuilder { 44 | mocker.verify( 45 | member: member, 46 | count: count, 47 | fileID: fileID, 48 | filePath: filePath, 49 | line: line, 50 | column: column 51 | ) 52 | return .init(mocker: mocker) 53 | } 54 | 55 | /// Asynchronously waits at most `timeout` interval for a successfuly assertion of 56 | /// `count` invocations, then fails. 57 | /// 58 | /// - Parameters: 59 | /// - count: Specifies the expected invocation count. 60 | /// - timeout: The maximum time it will wait for assertion to be true. Default 1 second. 61 | /// - Returns: The parent builder, used for chaining additional specifications. 62 | @discardableResult 63 | public func calledEventually(_ count: Count, 64 | before timeout: TimeoutDuration = .seconds(1), 65 | fileID: StaticString = #fileID, 66 | filePath: StaticString = #filePath, 67 | line: UInt = #line, 68 | column: UInt = #column) async -> ParentBuilder { 69 | await mocker.verify( 70 | member: member, 71 | count: count, 72 | timeout: timeout, 73 | fileID: fileID, 74 | filePath: filePath, 75 | line: line, 76 | column: column 77 | ) 78 | return .init(mocker: mocker) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/Mockable/Builder/FunctionBuilders/ThrowingFunctionActionBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThrowingFunctionActionBuilder.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 22.. 6 | // 7 | 8 | /// A builder for specifying actions to be performed when mocking a throwing function. 9 | /// 10 | /// This builder is used within the context of a higher-level builder (e.g., an `ActionBuilder`) 11 | /// to specify a desired action to perform when a particular throwing function of a mock service is called. 12 | public typealias ThrowingFunctionActionBuilder> 13 | = FunctionActionBuilder 14 | -------------------------------------------------------------------------------- /Sources/Mockable/Builder/FunctionBuilders/ThrowingFunctionReturnBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThrowingFunctionReturnBuilder.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 22.. 6 | // 7 | 8 | /// A builder for specifying return values or producers when mocking a function. 9 | /// 10 | /// This builder is typically used within the context of a higher-level builder (e.g., a `ReturnBuilder`) 11 | /// to specify the desired return value or a return value producer for a throwing function of a mock service. 12 | public struct ThrowingFunctionReturnBuilder< 13 | T: MockableService, 14 | ParentBuilder: Builder, 15 | ReturnType, 16 | ErrorType: Error, 17 | ProduceType 18 | > { 19 | 20 | /// Convenient type for the associated service's Member. 21 | public typealias Member = T.Member 22 | 23 | /// The member being mocked. 24 | private var member: Member 25 | 26 | /// The associated service's mocker. 27 | private var mocker: Mocker 28 | 29 | /// Initializes a new instance of `FunctionReturnBuilder`. 30 | /// 31 | /// - Parameters: 32 | /// - mocker: The `Mocker` instance of the associated mock service. 33 | /// - kind: The member being mocked. 34 | public init(_ mocker: Mocker, kind member: Member) { 35 | self.member = member 36 | self.mocker = mocker 37 | } 38 | 39 | /// Registers a return value to provide when the mocked member is called. 40 | /// 41 | /// - Parameter value: The return value to use for mocking the specified member. 42 | /// - Returns: The parent builder, used for chaining additional specifications. 43 | @discardableResult 44 | public func willReturn(_ value: ReturnType) -> ParentBuilder { 45 | mocker.addReturnValue(.return(value), for: member) 46 | return .init(mocker: mocker) 47 | } 48 | 49 | /// Registers an error to be thrown when the mocked member is called. 50 | /// 51 | /// - Parameter error: The error to be thrown for mocking the specified member. 52 | /// - Returns: The parent builder, used for chaining additional specifications. 53 | @discardableResult 54 | public func willThrow(_ error: ErrorType) -> ParentBuilder { 55 | mocker.addReturnValue(.throw(error), for: member) 56 | return .init(mocker: mocker) 57 | } 58 | 59 | /// Registers a return value producing closure to use when the mocked member is called. 60 | /// 61 | /// - Parameter producer: A closure that produces a return value or throws an error. 62 | /// - Returns: The parent builder, used for chaining additional specifications. 63 | @discardableResult 64 | public func willProduce(_ producer: ProduceType) -> ParentBuilder { 65 | mocker.addReturnValue(.produce(producer), for: member) 66 | return .init(mocker: mocker) 67 | } 68 | } 69 | 70 | extension ThrowingFunctionReturnBuilder where ReturnType == Void { 71 | /// Specifies that the void function will return normally when the mocked member is called. 72 | @discardableResult 73 | public func willReturn() -> ParentBuilder { 74 | mocker.addReturnValue(.return(()), for: member) 75 | return .init(mocker: mocker) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/Mockable/Builder/FunctionBuilders/ThrowingFunctionVerifyBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThrowingFunctionVerifyBuilder.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 22.. 6 | // 7 | 8 | /// A builder for verifying the number of times a throwing function was called. 9 | /// 10 | /// This builder is typically used within the context of a higher-level builder (e.g., a `VerifyBuilder`) 11 | /// to verify the expected number of invocations for a throwing function of a mock service. 12 | public typealias ThrowingFunctionVerifyBuilder> 13 | = FunctionVerifyBuilder 14 | -------------------------------------------------------------------------------- /Sources/Mockable/Builder/MockableService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockableService.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 13.. 6 | // 7 | 8 | /// A protocol defining the structure for a mocked service. 9 | /// 10 | /// Conforming types must provide a `Member` type representing their members 11 | /// as well as builders for specifying return values, actions, and verifications. 12 | public protocol MockableService { 13 | 14 | /// A type representing the members of the mocked protocol. 15 | associatedtype Member: Matchable, CaseIdentifiable, Sendable 16 | 17 | /// A builder responsible for registering return values. 18 | associatedtype ReturnBuilder: Builder 19 | /// A builder responsible for registering side-effects. 20 | associatedtype ActionBuilder: Builder 21 | /// A builder responsible for asserting member invocations. 22 | associatedtype VerifyBuilder: Builder 23 | 24 | /// Encapsulates member return values. 25 | typealias Return = MemberReturn 26 | 27 | /// Encapsulates member side-effects. 28 | typealias Action = MemberAction 29 | 30 | /// A builder proxy for specifying return values. 31 | var _given: ReturnBuilder { get } 32 | 33 | /// A builder proxy for specifying actions. 34 | var _when: ActionBuilder { get } 35 | 36 | /// The builder proxy for verifying invocations. 37 | var _verify: VerifyBuilder { get } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Mockable/Builder/PropertyBuilders/PropertyActionBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PropertyActionBuilder.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 22.. 6 | // 7 | 8 | /// A builder for specifying actions to be performed when mocking the getter and setter of a property. 9 | /// 10 | /// This builder is typically used within the context of a higher-level builder (e.g., an `ActionBuilder`) 11 | /// to specify the behavior of the getter and setter of a particular property of a mock. 12 | public struct PropertyActionBuilder> { 13 | 14 | /// Convenient type for the associated service's Member. 15 | public typealias Member = T.Member 16 | 17 | /// The member representing the getter of the property. 18 | var getMember: Member 19 | 20 | /// The member representing the setter of the property. 21 | var setMember: Member 22 | 23 | /// The associated service's mocker. 24 | var mocker: Mocker 25 | 26 | /// Initializes a new instance of `PropertyActionBuilder`. 27 | /// 28 | /// - Parameters: 29 | /// - mocker: The `Mocker` instance of the associated mock service. 30 | /// - getKind: The member representing the getter of the property. 31 | /// - setKind: The member representing the setter of the property. 32 | public init(_ mocker: Mocker, kind getMember: Member, setKind setMember: Member) { 33 | self.getMember = getMember 34 | self.setMember = setMember 35 | self.mocker = mocker 36 | } 37 | 38 | /// Specifies the action to be performed when the getter of the property is called. 39 | /// 40 | /// - Parameter action: The closure representing the action to be performed. 41 | /// - Returns: The parent builder, typically used for chaining additional specifications. 42 | @discardableResult 43 | public func performOnGet(_ action: @escaping () -> Void) -> ParentBuilder { 44 | mocker.addAction(action, for: getMember) 45 | return .init(mocker: mocker) 46 | } 47 | 48 | /// Specifies the action to be performed when the setter of the property is called. 49 | /// 50 | /// - Parameter action: The closure representing the action to be performed. 51 | /// - Returns: The parent builder, typically used for chaining additional specifications. 52 | @discardableResult 53 | public func performOnSet(_ action: @escaping () -> Void) -> ParentBuilder { 54 | mocker.addAction(action, for: setMember) 55 | return .init(mocker: mocker) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/Mockable/Builder/PropertyBuilders/PropertyReturnBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PropertyReturnBuilder.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 22.. 6 | // 7 | 8 | /// A builder for specifying return values or producers when mocking the getter of a mutable property. 9 | /// 10 | /// This builder is typically used within the context of a higher-level builder (e.g., a `ReturnBuilder`) 11 | /// to specify the desired return value or a return value producer for the getter 12 | /// of a particular property of a mock. 13 | public struct PropertyReturnBuilder, ReturnType> { 14 | 15 | /// Convenient type for the associated service's Member. 16 | public typealias Member = T.Member 17 | 18 | /// The member representing the getter of the property. 19 | var getMember: Member 20 | 21 | /// The associated service's mocker. 22 | var mocker: Mocker 23 | 24 | /// Initializes a new instance of `PropertyReturnBuilder`. 25 | /// 26 | /// - Parameters: 27 | /// - mocker: The `Mocker` instance of the associated mock service. 28 | /// - getKind: The member representing the getter of the property. 29 | public init(_ mocker: Mocker, kind getMember: Member) { 30 | self.getMember = getMember 31 | self.mocker = mocker 32 | } 33 | 34 | /// Specifies the return value when the getter of the property is called. 35 | /// 36 | /// - Parameter value: The return value to use for mocking the specified getter. 37 | /// - Returns: The parent builder, used for chaining additional specifications. 38 | @discardableResult 39 | public func willReturn(_ value: ReturnType) -> ParentBuilder { 40 | mocker.addReturnValue(.return(value), for: getMember) 41 | return .init(mocker: mocker) 42 | } 43 | 44 | /// Specifies the return value producing closure to use when the getter of the property is called. 45 | /// 46 | /// - Parameter producer: A closure that produces a return value for the getter. 47 | /// - Returns: The parent builder, used for chaining additional specifications. 48 | @discardableResult 49 | public func willProduce(_ producer: @escaping () -> ReturnType) -> ParentBuilder { 50 | mocker.addReturnValue(.produce(producer), for: getMember) 51 | return .init(mocker: mocker) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Mockable/Builder/PropertyBuilders/PropertyVerifyBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PropertyVerifyBuilder.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 22.. 6 | // 7 | 8 | /// A builder for verifying the number of times the getter and setter of a property are called. 9 | /// 10 | /// This builder is typically used within the context of a higher-level builder (e.g., a `VerifyBuilder`) 11 | /// to verify the expected number of invocations for the getter and setter of a particular property of a mock. 12 | public struct PropertyVerifyBuilder> { 13 | 14 | /// Convenient type for the associated service's Member. 15 | public typealias Member = T.Member 16 | 17 | /// The associated service's mocker. 18 | var mocker: Mocker 19 | 20 | /// The member representing the getter of the property. 21 | var getMember: Member 22 | 23 | /// The member representing the setter of the property. 24 | var setMember: Member 25 | 26 | /// Initializes a new instance of `PropertyVerifyBuilder`. 27 | /// 28 | /// - Parameters: 29 | /// - mocker: The `Mocker` instance of the associated mock service. 30 | /// - getKind: The member representing the getter of the property. 31 | /// - setKind: The member representing the setter of the property. 32 | public init(_ mocker: Mocker, 33 | kind getMember: Member, 34 | setKind setMember: Member) { 35 | self.getMember = getMember 36 | self.setMember = setMember 37 | self.mocker = mocker 38 | } 39 | 40 | /// Specifies the expected number of times the getter of the property should be called. 41 | /// 42 | /// - Parameter count: The `Count` object specifying the expected invocation count. 43 | /// - Returns: The parent builder, used for chaining additional specifications. 44 | @discardableResult 45 | public func getCalled( 46 | _ count: Count, 47 | fileID: StaticString = #fileID, 48 | filePath: StaticString = #filePath, 49 | line: UInt = #line, 50 | column: UInt = #column 51 | ) -> ParentBuilder { 52 | mocker.verify( 53 | member: getMember, 54 | count: count, 55 | fileID: fileID, 56 | filePath: filePath, 57 | line: line, 58 | column: column 59 | ) 60 | return .init(mocker: mocker) 61 | } 62 | 63 | /// Asynchronously waits at most `timeout` interval for a successfuly assertion of 64 | /// `count` invocations of the property's getter, then fails. 65 | /// 66 | /// - Parameters: 67 | /// - count: Specifies the expected invocation count. 68 | /// - timeout: The maximum time it will wait for assertion to be true. Default 1 second. 69 | /// - Returns: The parent builder, used for chaining additional specifications. 70 | @discardableResult 71 | public func getCalledEventually(_ count: Count, 72 | before timeout: TimeoutDuration = .seconds(1), 73 | fileID: StaticString = #fileID, 74 | filePath: StaticString = #filePath, 75 | line: UInt = #line, 76 | column: UInt = #column) async -> ParentBuilder { 77 | await mocker.verify( 78 | member: getMember, 79 | count: count, 80 | timeout: timeout, 81 | fileID: fileID, 82 | filePath: filePath, 83 | line: line, 84 | column: column 85 | ) 86 | return .init(mocker: mocker) 87 | } 88 | 89 | /// Specifies the expected number of times the setter of the property should be called. 90 | /// 91 | /// - Parameter count: The `Count` object specifying the expected invocation count. 92 | /// - Returns: The parent builder, used for chaining additional specifications. 93 | @discardableResult 94 | public func setCalled( 95 | _ count: Count, 96 | fileID: StaticString = #fileID, 97 | filePath: StaticString = #filePath, 98 | line: UInt = #line, 99 | column: UInt = #column) -> ParentBuilder { 100 | mocker.verify( 101 | member: setMember, 102 | count: count, 103 | fileID: fileID, 104 | filePath: filePath, 105 | line: line, 106 | column: column 107 | ) 108 | return .init(mocker: mocker) 109 | } 110 | 111 | /// Asynchronously waits at most `timeout` interval for a successfuly assertion of 112 | /// `count` invocations fot the property's setter, then fails. 113 | /// 114 | /// - Parameters: 115 | /// - count: Specifies the expected invocation count. 116 | /// - timeout: The maximum time it will wait for assertion to be true. Default 1 second. 117 | /// - Returns: The parent builder, used for chaining additional specifications. 118 | @discardableResult 119 | public func setCalledEventually(_ count: Count, 120 | before timeout: TimeoutDuration = .seconds(1), 121 | fileID: StaticString = #fileID, 122 | filePath: StaticString = #filePath, 123 | line: UInt = #line, 124 | column: UInt = #column) async -> ParentBuilder { 125 | await mocker.verify( 126 | member: setMember, 127 | count: count, 128 | timeout: timeout, 129 | fileID: fileID, 130 | filePath: filePath, 131 | line: line, 132 | column: column 133 | ) 134 | return .init(mocker: mocker) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Sources/Mockable/Documentation/Documentation.docc/Configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Learn how to configure build settings so generated mock implementations are excluded from release builds. 4 | 5 | ## Overview 6 | 7 | Since `@Mockable` is a peer macro the generated code will always be at the same scope as the protocol it is attached to. From **Mockable**'s perspective this is unfortunate as we don't want to include our generated mock implementations in the release bundle. 8 | 9 | To solve this, the macro expansion is enclosed in a pre-defined compile-time flag called **`MOCKING`** that can be leveraged to exclude generated mock implementations from release builds: 10 | ```swift 11 | #if MOCKING 12 | public final class MockService: Service, Mockable { 13 | // generated code... 14 | } 15 | #endif 16 | ``` 17 | 18 | > Since the **`MOCKING`** flag is not defined in your project by default, you won't be able to use mock implementations unless you configure it. 19 | 20 | There are many ways to define the flag depending on how your project is set up or what tool you use for build setting generation. Below you can find how to define the `MOCKING` flag in three common scenarios. 21 | 22 | ## Define the flag using Xcode 23 | If your porject relies on Xcode build settings, define the flag in your target's build settings for debug build configuration(s): 24 | 1. Open your **Xcode project**. 25 | 2. Go to your target's **Build Settings**. 26 | 3. Find **Swift Compiler - Custom Flags**. 27 | 4. Add the **MOCKING** flag under the debug build configuration(s). 28 | 5. Repeat these steps for all of your targets where you want to use the `@Mockable` macro. 29 | 30 | ## Using a Package.swift manifest 31 | If you are using SPM modules or working with a package, define the **`MOCKING`** compile-time condition in your package manifest. Using a `.when(configuration:)` build setting condition you can define the flag only if the build configuration is set to `debug`. 32 | ```swift 33 | .target( 34 | ... 35 | swiftSettings: [ 36 | .define("MOCKING", .when(configuration: .debug)) 37 | ] 38 | ) 39 | ``` 40 | 41 | ## Using XcodeGen 42 | If you use XcodeGen to generate you project, you can define the `MOCKING` flag in your yaml file under the `configs` definition. 43 | ```yml 44 | settings: 45 | ... 46 | configs: 47 | debug: 48 | SWIFT_ACTIVE_COMPILATION_CONDITIONS: MOCKING 49 | ``` 50 | 51 | ## Using [Tuist](https://tuist.io/): 52 | 53 | If you use Tuist, you can define the `MOCKING` flag in your target's settings under `configurations`. 54 | ```swift 55 | .target( 56 | ... 57 | settings: .settings( 58 | configurations: [ 59 | .debug( 60 | name: .debug, 61 | settings: [ 62 | "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "$(inherited) MOCKING" 63 | ] 64 | ) 65 | ] 66 | ) 67 | ) 68 | ``` 69 | 70 | ## Summary 71 | 72 | By defining the `MOCKING` condition in the debug build configuration you ensured that generated mock implementations are excluded from release builds and kept available for your unit tests that use the debug configuration to build tested targets. 73 | -------------------------------------------------------------------------------- /Sources/Mockable/Documentation/Documentation.docc/Installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Learn how to install **Mockable** and integrate into your targets. 4 | 5 | ## Overview 6 | 7 | **Mockable** can be installed using Swift Package Manager. 8 | 9 | Add the **Mockable** target to all targets that contain protocols you want to mock. **Mockable** does not depend on the `XCTest` framework so it can be added to any target. 10 | 11 | ## Add **Mockable** using Xcode 12 | 1. Open your Xcode project. 13 | 2. Navigate to the **File** menu and select **Add Package Dependencies...** 14 | 3. Enter the following URL: **`https://github.com/Kolos65/Mockable`** 15 | 4. Specify the version you want to use and click **Add Package**. 16 | 6. Click **Add Package** 17 | 18 | > If you have multiple targets or multiple test targets: 19 | > Navigate to each target's **General** settings and add Mockable under the **Frameworks and Libraries** settings. 20 | 21 | ### Using a Package.swift manifest: 22 | If you have SPM modules or you want to test an SPM package, add **Mockable** as a package dependency in the manifest file. 23 | 24 | In you target definitions add the **Mockable** product to your main and test targets. 25 | ```swift 26 | let package = Package( 27 | ... 28 | dependencies: [ 29 | .package(url: "https://github.com/Kolos65/Mockable", from: "0.0.1"), 30 | ], 31 | targets: [ 32 | .target( 33 | ... 34 | dependencies: [ 35 | .product(name: "Mockable", package: "Mockable") 36 | ] 37 | ), 38 | .testTarget( 39 | ... 40 | dependencies: [ 41 | .product(name: "Mockable", package: "Mockable") 42 | ] 43 | ) 44 | ] 45 | ) 46 | ``` 47 | 48 | ### Using XcodeGen: 49 | Add Mockable to the `packages` definition and the `targets` definition. 50 | ```yaml 51 | packages: 52 | Mockable: 53 | url: https://github.com/Kolos65/Mockable 54 | from: "0.0.1" 55 | 56 | targets: 57 | MyApp: 58 | ... 59 | dependencies: 60 | - package: Mockable 61 | product: Mockable 62 | MyAppUnitTests: 63 | ... 64 | dependencies: 65 | - package: Mockable 66 | product: Mockable 67 | 68 | ``` 69 | -------------------------------------------------------------------------------- /Sources/Mockable/Documentation/Documentation.docc/Mockable.md: -------------------------------------------------------------------------------- 1 | # ``Mockable`` 2 | 3 | A macro driven testing framework that provides automatic mock implementations for your protocols. It offers an intuitive declarative syntax that simplifies the process of mocking services in unit tests. 4 | 5 | ## Overview 6 | 7 | **Mockable** utilizes the new Swift macro system to generate code that eliminates the need for external dependencies like Sourcery. 8 | It has a declarative API that enables you to rapidly specify return values and verify invocations in a readable format. 9 | 10 | 11 | Associated types, generic functions, where clauses and constrained generic arguments are all supported. 12 | The generated mock implementations can be excluded from release builds using a built in compile condition. 13 | 14 | ## Topics 15 | 16 | ### Guides 17 | 18 | - 19 | - 20 | - 21 | -------------------------------------------------------------------------------- /Sources/Mockable/Helpers/Async+Timeout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Async+Timeout.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2024. 04. 04.. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct TimeoutError: Error {} 11 | 12 | /// Runs an async task with a timeout. 13 | /// 14 | /// - Parameters: 15 | /// - maxDuration: The duration in seconds `work` is allowed to run before timing out. 16 | /// - work: The async operation to perform. 17 | /// - Returns: Returns the result of `work` if it completed in time. 18 | /// - Throws: Throws ``TimedOutError`` if the timeout expires before `work` completes. 19 | /// If `work` throws an error before the timeout expires, that error is propagated to the caller. 20 | @discardableResult 21 | func withTimeout( 22 | after maxDuration: TimeInterval, 23 | _ operation: @Sendable @escaping () async throws -> Value 24 | ) async throws -> Value { 25 | try await withThrowingTaskGroup(of: Value.self) { group in 26 | group.addTask(operation: operation) 27 | group.addTask { 28 | try await Task.sleep(nanoseconds: UInt64(maxDuration * 1_000_000_000)) 29 | throw TimeoutError() 30 | } 31 | let result = try await group.next()! // swiftlint:disable:this force_unwrapping 32 | group.cancelAll() 33 | return result 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Mockable/Helpers/AsyncSubject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncSubject.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2024. 12. 16.. 6 | // 7 | 8 | actor AsyncSubject: AsyncSequence { 9 | 10 | typealias Failure = Never 11 | 12 | // MARK: Types 13 | 14 | struct Subscription: Equatable { 15 | let id: UInt64 16 | let continuation: AsyncStream.Continuation 17 | let stream: AsyncStream 18 | static func == (lhs: Self, rhs: Self) -> Bool { lhs.id == rhs.id } 19 | } 20 | 21 | // MARK: Private properties 22 | 23 | private(set) var value: Element 24 | private var ids: UInt64 = 0 25 | private var subscriptions = [Subscription]() 26 | 27 | // MARK: Init 28 | 29 | init(_ initialValue: Element) { 30 | self.value = initialValue 31 | } 32 | 33 | deinit { 34 | for subscription in subscriptions { 35 | subscription.continuation.finish() 36 | } 37 | } 38 | 39 | func finish() { 40 | for subscription in subscriptions { 41 | subscription.continuation.finish() 42 | } 43 | subscriptions.removeAll() 44 | } 45 | 46 | nonisolated func makeAsyncIterator() -> AsyncIterator { 47 | AsyncIterator(parent: self) 48 | } 49 | 50 | func generateId() -> UInt64 { 51 | defer { ids &+= 1 } 52 | return ids 53 | } 54 | 55 | fileprivate func subscribe() -> (Subscription, Element) { 56 | let (stream, continuation) = AsyncStream.makeStream() 57 | let subscription = Subscription(id: generateId(), continuation: continuation, stream: stream) 58 | subscriptions.append(subscription) 59 | return (subscription, value) 60 | } 61 | 62 | fileprivate func remove(_ subscription: Subscription) { 63 | subscriptions.removeAll { $0 == subscription } 64 | subscription.continuation.finish() 65 | } 66 | 67 | func send(_ value: Element) { 68 | self.value = value 69 | for subscription in subscriptions { 70 | subscription.continuation.yield(value) 71 | } 72 | } 73 | 74 | func update(with block: (inout Element) -> Void) { 75 | block(&value) 76 | send(value) 77 | } 78 | 79 | class AsyncIterator: AsyncIteratorProtocol { 80 | private weak var parent: AsyncSubject? 81 | private var subscription: Subscription? 82 | private var iterator: AsyncStream.AsyncIterator? 83 | 84 | fileprivate init(parent: AsyncSubject) { 85 | self.parent = parent 86 | } 87 | 88 | deinit { 89 | cancelSubscription() 90 | } 91 | 92 | func next() async -> Element? { 93 | if iterator != nil { 94 | return await iterator?.next() 95 | } else if let parent { 96 | let (subscription, value) = await parent.subscribe() 97 | self.subscription = subscription 98 | iterator = subscription.stream.makeAsyncIterator() 99 | return value 100 | } else { 101 | return nil 102 | } 103 | } 104 | 105 | private func cancelSubscription() { 106 | guard let parent, let subscription else { return } 107 | Task { await parent.remove(subscription) } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/Mockable/Helpers/LockedValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LockedValue.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2024. 12. 16.. 6 | // 7 | import Foundation 8 | 9 | /// A generic wrapper for isolating a mutable value with a lock. 10 | /// 11 | /// If you trust the sendability of the underlying value, consider using ``UncheckedSendable``, 12 | /// instead. 13 | @dynamicMemberLookup 14 | final class LockedValue: @unchecked Sendable { 15 | private var _value: Value 16 | private let lock = NSRecursiveLock() 17 | private var didSet: ((Value) -> Void)? 18 | 19 | /// Initializes lock-isolated state around a value. 20 | /// 21 | /// - Parameter value: A value to isolate with a lock. 22 | init(_ value: @autoclosure @Sendable () throws -> Value) rethrows { 23 | self._value = try value() 24 | } 25 | 26 | subscript(dynamicMember keyPath: KeyPath) -> Subject { 27 | self.lock.criticalRegion { 28 | self._value[keyPath: keyPath] 29 | } 30 | } 31 | 32 | /// Perform an operation with isolated access to the underlying value. 33 | /// 34 | /// Useful for modifying a value in a single transaction. 35 | /// 36 | /// ```swift 37 | /// // Isolate an integer for concurrent read/write access: 38 | /// var count = LockedValue(0) 39 | /// 40 | /// func increment() { 41 | /// // Safely increment it: 42 | /// self.count.withValue { $0 += 1 } 43 | /// } 44 | /// ``` 45 | /// 46 | /// - Parameter operation: An operation to be performed on the the underlying value with a lock. 47 | /// - Returns: The result of the operation. 48 | func withValue( 49 | _ operation: (inout Value) throws -> T 50 | ) rethrows -> T { 51 | try self.lock.criticalRegion { 52 | var value = self._value 53 | defer { 54 | self._value = value 55 | self.didSet?(self._value) 56 | } 57 | return try operation(&value) 58 | } 59 | } 60 | 61 | /// Overwrite the isolated value with a new value. 62 | /// 63 | /// ```swift 64 | /// // Isolate an integer for concurrent read/write access: 65 | /// var count = LockedValue(0) 66 | /// 67 | /// func reset() { 68 | /// // Reset it: 69 | /// self.count.setValue(0) 70 | /// } 71 | /// ``` 72 | /// 73 | /// > Tip: Use ``withValue(_:)`` instead of ``setValue(_:)`` if the value being set is derived 74 | /// > from the current value. That is, do this: 75 | /// > 76 | /// > ```swift 77 | /// > self.count.withValue { $0 += 1 } 78 | /// > ``` 79 | /// > 80 | /// > ...and not this: 81 | /// > 82 | /// > ```swift 83 | /// > self.count.setValue(self.count + 1) 84 | /// > ``` 85 | /// > 86 | /// > ``withValue(_:)`` isolates the entire transaction and avoids data races between reading and 87 | /// > writing the value. 88 | /// 89 | /// - Parameter newValue: The value to replace the current isolated value with. 90 | func setValue(_ newValue: @autoclosure () throws -> Value) rethrows { 91 | try self.lock.criticalRegion { 92 | self._value = try newValue() 93 | self.didSet?(self._value) 94 | } 95 | } 96 | } 97 | 98 | extension LockedValue where Value: Sendable { 99 | var value: Value { 100 | self.lock.criticalRegion { 101 | self._value 102 | } 103 | } 104 | 105 | /// Initializes lock-isolated state around a value. 106 | /// 107 | /// - Parameter value: A value to isolate with a lock. 108 | /// - Parameter didSet: A callback to invoke when the value changes. 109 | convenience init( 110 | _ value: @autoclosure @Sendable () throws -> Value, 111 | didSet: (@Sendable (Value) -> Void)? = nil 112 | ) rethrows { 113 | try self.init(value()) 114 | self.didSet = didSet 115 | } 116 | } 117 | 118 | extension NSRecursiveLock { 119 | @inlinable @discardableResult 120 | func criticalRegion(work: () throws -> R) rethrows -> R { 121 | self.lock() 122 | defer { self.unlock() } 123 | return try work() 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/Mockable/Macro/MockableMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockableMacro.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 20.. 6 | // 7 | 8 | /// A peer macro that generates a mock implementation for the protocol it is attached to. 9 | /// 10 | /// The generated implementation is named with a "Mock" prefix followed by the protocol name. 11 | /// By default, the generated code is enclosed in an `#if MOCKING` condition, ensuring it is only accessible 12 | /// in modules where the `MOCKING` compile-time condition is set. 13 | /// 14 | /// Example usage: 15 | /// 16 | /// ```swift 17 | /// @Mockable 18 | /// protocol UserService { 19 | /// func get(id: UUID) -> User 20 | /// func remove(id: UUID) throws 21 | /// } 22 | /// 23 | /// var mockUserService: MockUserService 24 | /// 25 | /// func test() { 26 | /// let error: UserError = .invalidId 27 | /// let mockUser = User(id: UUID()) 28 | /// 29 | /// given(mockUserService) 30 | /// .get(id: .any).willReturn(mockUser) 31 | /// .remove(id: .any).willThrow(error) 32 | /// 33 | /// try await loginService.login() 34 | /// 35 | /// verify(mockUserService) 36 | /// .get(id: .value(mockUser.id)).called(count: .once) 37 | /// .remove(id: .any).called(count: .never) 38 | /// } 39 | /// ``` 40 | @attached(peer, names: prefixed(Mock)) 41 | public macro Mockable() = #externalMacro(module: "MockableMacro", type: "MockableMacro") 42 | -------------------------------------------------------------------------------- /Sources/Mockable/Mocker/CaseIdentifiable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaseIdentifiable.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 22.. 6 | // 7 | 8 | /// A protocol for enumerations that can be identified by a unique identifier. 9 | /// 10 | /// Enumerations conforming to `CaseIdentifiable` must provide a computed property `id` 11 | /// that returns a unique identifier, typically derived from the type name. 12 | public protocol CaseIdentifiable: Equatable, Hashable { 13 | /// A computed property that returns a unique identifier for the case. 14 | var id: String { get } 15 | 16 | /// A computed property that returns a human readably name for member enum cases. 17 | var name: String { get } 18 | 19 | /// A computed property that returns a human readably description of member parameters. 20 | var parameters: String { get } 21 | } 22 | 23 | extension CaseIdentifiable { 24 | /// A default implementation of the `id` property, deriving the identifier from the case name. 25 | /// 26 | /// The default implementation removes any additional information such as parameters 27 | /// by splitting the type name at the opening parenthesis. 28 | public var id: String { 29 | String(String(describing: self).split(separator: "(")[0]) 30 | } 31 | 32 | /// A default implementation of the `name` property, deriving a human readable name for member enum cases. 33 | public var name: String { 34 | guard let lastToken = id.split(separator: "_").last else { return id } 35 | return String(lastToken) 36 | } 37 | 38 | /// A default implementation of the `parameters` property, deriving a human readable name for member parameters. 39 | public var parameters: String { 40 | let description = String(describing: self) 41 | guard let index = description.firstIndex(of: "(") else { return "no parameters" } 42 | return String(description[index...]) 43 | } 44 | } 45 | 46 | extension CaseIdentifiable { 47 | /// Compares two `CaseIdentifiable` instances for equality based on their unique identifiers. 48 | /// 49 | /// - Parameters: 50 | /// - lhs: The left-hand side `CaseIdentifiable`. 51 | /// - rhs: The right-hand side `CaseIdentifiable`. 52 | /// - Returns: `true` if the instances have the same identifier; otherwise, `false`. 53 | public static func == (lhs: Self, rhs: Self) -> Bool { 54 | lhs.id == rhs.id 55 | } 56 | 57 | /// Hashes a `CaseIdentifiable` instance using its unique identifier. 58 | /// 59 | /// - Parameter hasher: The hasher to use for combining the hash value. 60 | public func hash(into hasher: inout Hasher) { 61 | hasher.combine(id) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/Mockable/Mocker/Matchable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Matchable.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 14.. 6 | // 7 | 8 | /// A protocol for types that can be used as matchers in mock assertions. 9 | public protocol Matchable { 10 | /// Determines if the receiver matches another instance of the same type according to custom criteria. 11 | /// 12 | /// - Parameter other: The instance to compare against. 13 | /// - Returns: `true` if the receiver matches the specified instance; otherwise, `false`. 14 | func match(_ other: Self) -> Bool 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Mockable/Mocker/MemberAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemberAction.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 14.. 6 | // 7 | 8 | /// A class representing an action to be performed on a member. 9 | /// 10 | /// `MemberAction` associates a member of type `Member` with a closure (`action`) 11 | /// that can be executed when needed. 12 | public class MemberAction { 13 | /// The member associated with the action. 14 | public let member: Member 15 | 16 | /// The closure representing the action to be performed. 17 | public let action: () -> Void 18 | 19 | /// Initializes a new instance of `MemberAction`. 20 | /// 21 | /// - Parameters: 22 | /// - member: The member to associate with the action. 23 | /// - action: The closure representing the action to be performed. 24 | public init(member: Member, action: @escaping () -> Void) { 25 | self.member = member 26 | self.action = action 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Mockable/Mocker/MemberReturn.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemberReturn.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 14.. 6 | // 7 | 8 | /// A class representing the return value associated with a member. 9 | /// 10 | /// `MemberReturn` associates a member of type `Member` with a `ReturnValue` object, 11 | /// encapsulating the information about the expected return value or behavior. 12 | public class MemberReturn { 13 | /// The member associated with the return value. 14 | public let member: Member 15 | 16 | /// The `ReturnValue` object encapsulating information about the expected return value or behavior. 17 | public let returnValue: ReturnValue 18 | 19 | /// Initializes a new instance of `MemberReturn`. 20 | /// 21 | /// - Parameters: 22 | /// - member: The member to associate with the return value. 23 | /// - returnValue: The `ReturnValue` object representing the expected return value or behavior. 24 | public init(member: Member, returnValue: ReturnValue) { 25 | self.member = member 26 | self.returnValue = returnValue 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Mockable/Mocker/Mocked.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mocked.swift 3 | // Mocked 4 | // 5 | // Created by Kolos Foltanyi on 2024. 04. 03.. 6 | // 7 | 8 | /// A protocol that represents auto-mocked types. 9 | /// 10 | /// `Mocked` in combination with a `relaxedMocked` option of `MockerPolicy `can be used 11 | /// to set an implicit return value for custom types: 12 | /// 13 | /// ```swift 14 | /// struct Car { 15 | /// var name: String 16 | /// var seats: Int 17 | /// } 18 | /// 19 | /// extension Car: Mocked { 20 | /// static var mock: Car { 21 | /// Car(name: "Mock Car", seats: 4) 22 | /// } 23 | /// 24 | /// // Defaults to [mock] but we can 25 | /// // provide a custom array of cars: 26 | /// static var mocks: [Car] { 27 | /// [ 28 | /// Car(name: "Mock Car 1", seats: 4), 29 | /// Car(name: "Mock Car 2", seats: 4) 30 | /// ] 31 | /// } 32 | /// } 33 | /// 34 | /// @Mockable 35 | /// protocol CarService { 36 | /// func getCar() -> Car 37 | /// func getCars() -> [Car] 38 | /// } 39 | /// 40 | /// func testCarService() { 41 | /// func test() { 42 | /// let mock = MockCarService(policy: .relaxedMocked) 43 | /// // Implictly mocked without a given registration: 44 | /// let car = mock.getCar() 45 | /// let cars = mock.getCars() 46 | /// } 47 | /// } 48 | /// ``` 49 | public protocol Mocked { 50 | /// A default mock return value to use when `.relaxedMocked` policy is set. 51 | static var mock: Self { get } 52 | 53 | /// An array of mock values to use as return values when `.relaxedMocked` policy is set. 54 | /// Defaults to `[Self.mock]`. 55 | static var mocks: [Self] { get } 56 | } 57 | 58 | extension Mocked { 59 | public static var mocks: [Self] { [mock] } 60 | } 61 | 62 | extension Array: Mocked where Element: Mocked { 63 | public static var mock: Self { 64 | Element.mocks 65 | } 66 | } 67 | 68 | extension Optional: Mocked where Wrapped: Mocked { 69 | public static var mock: Self { Wrapped.mock } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Mockable/Mocker/MockerFallback.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockerFallback.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 17.. 6 | // 7 | 8 | /// Describes an optional default value to use when no stored 9 | /// return value found during mocking. 10 | enum MockerFallback { 11 | /// Specifies a default value to be used when no stored return value is found. 12 | case value(V) 13 | 14 | /// Specifies that no default value should be used when no stored return value is found. 15 | /// This results in a fatal error. This is the default behavior. 16 | case none 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Mockable/Mocker/MockerPolicy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockerPolicy.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2024. 04. 01.. 6 | // 7 | 8 | /// A policy that controls how the library handles when no return value is found during mocking. 9 | /// 10 | /// MockerPolicy can be used to customize mocking behavior and disable the requirement of 11 | /// return value registration in case of certain types. 12 | public struct MockerPolicy: OptionSet, Sendable { 13 | /// Default policy to use when none was explicitly specified for a mock. 14 | /// 15 | /// Change this property to set the default policy to use for all mocks. Defaults to `strict`. 16 | #if swift(>=6) 17 | nonisolated(unsafe) public static var `default`: Self = .strict 18 | #else 19 | public static var `default`: Self = .strict 20 | #endif 21 | 22 | /// All return values must be registered, a fatal error will occur otherwise. 23 | public static let strict: Self = [] 24 | 25 | /// Every literal expressible requirement will return a default value. 26 | public static let relaxed: Self = [ 27 | .relaxedOptional, 28 | .relaxedThrowingVoid, 29 | .relaxedNonThrowingVoid, 30 | .relaxedMocked 31 | ] 32 | 33 | /// Every void function will run normally without a registration 34 | public static let relaxedVoid: Self = [ 35 | .relaxedNonThrowingVoid, 36 | .relaxedThrowingVoid 37 | ] 38 | 39 | /// Throwing Void functions will run without return value registration. 40 | public static let relaxedThrowingVoid = Self(rawValue: 1 << 0) 41 | 42 | /// Non-throwing Void functions will run without return value registration. 43 | public static let relaxedNonThrowingVoid = Self(rawValue: 1 << 1) 44 | 45 | /// Optional return values will default to nil. 46 | public static let relaxedOptional = Self(rawValue: 1 << 2) 47 | 48 | /// Types conforming to the `Mocked` protocol will default to their mock value. 49 | public static let relaxedMocked = Self(rawValue: 1 << 3) 50 | 51 | /// Option set raw value. 52 | public let rawValue: Int 53 | 54 | /// Creates a new option set from the given raw value. 55 | public init(rawValue: Int) { 56 | self.rawValue = rawValue 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Mockable/Mocker/MockerScope.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockerScope.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 21.. 6 | // 7 | 8 | /// An enumeration representing different scopes of the Mocker state. 9 | /// 10 | /// Scopes can be used to only reset specific states of a mock service. 11 | public enum MockerScope: CaseIterable { 12 | /// The scope for storing expected return values. 13 | case given 14 | /// The scope for storing actions to be performed on members. 15 | case when 16 | /// The scope for storing invocations to be verified. 17 | case verify 18 | } 19 | 20 | extension Set where Element == MockerScope { 21 | /// A convenience property representing a set containing all available scopes. 22 | public static var all: Self { Set(MockerScope.allCases) } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Mockable/Models/Count.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Count.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 13.. 6 | // 7 | 8 | /// An enumeration representing different counting conditions for verifying invocations. 9 | /// 10 | /// Use `Count` in `verify` clauses to write assertions: 11 | /// ```swift 12 | /// // Assert `fetch(for)` was called between 1 and 5 times: 13 | /// verify(productService) 14 | /// .fetch(for: .any) 15 | /// .called(.from(1, to: 5)) 16 | /// ``` 17 | public enum Count: ExpressibleByIntegerLiteral, Sendable { 18 | /// The associated type for the integer literal. 19 | public typealias IntegerLiteralType = Int 20 | 21 | /// Initializes a new instance of `Count` using an integer literal. 22 | /// 23 | /// - Parameter value: The integer literal value. 24 | public init(integerLiteral value: IntegerLiteralType) { 25 | self = .exactly(value) 26 | } 27 | 28 | /// The member was called at least once. 29 | case atLeastOnce 30 | /// The member was called exactly once. 31 | case once 32 | /// The member was called a specific number of times. 33 | case exactly(Int) 34 | /// The member was called within a specific range of times. 35 | case from(Int, to: Int) 36 | /// The member was called less than a specific number of times. 37 | case less(than: Int) 38 | /// The member was called less than or equal to a specific number of times. 39 | case lessOrEqual(to: Int) 40 | /// The member was called more than a specific number of times. 41 | case more(than: Int) 42 | /// The member was called more than or equal to a specific number of times. 43 | case moreOrEqual(to: Int) 44 | /// The member was never called. 45 | case never 46 | 47 | /// Checks if the given count satisfies the specified condition. 48 | /// 49 | /// - Parameter count: The actual count to be compared. 50 | /// - Returns: `true` if the condition is satisfied; otherwise, `false`. 51 | func satisfies(_ count: Int) -> Bool { 52 | switch self { 53 | case .atLeastOnce: return count >= 1 54 | case .once: return count == 1 55 | case .exactly(let times): return count == times 56 | case .from(let from, let to): return count >= from && count <= to 57 | case .less(let than): return count < than 58 | case .lessOrEqual(let to): return count <= to 59 | case .more(let than): return count > than 60 | case .moreOrEqual(let to): return count >= to 61 | case .never: return count == 0 62 | } 63 | } 64 | } 65 | 66 | extension Count: CustomStringConvertible { 67 | public var description: String { 68 | switch self { 69 | case .atLeastOnce: 70 | return "at least 1" 71 | case .once: 72 | return "exactly one" 73 | case .exactly(let value): 74 | return "exactly \(value)" 75 | case .from(let lowerBound, let upperBound): 76 | return "from \(lowerBound) to \(upperBound)" 77 | case .less(let value): 78 | return "less than \(value)" 79 | case .lessOrEqual(let value): 80 | return "less than or equal to \(value)" 81 | case .more(let value): 82 | return "more than \(value)" 83 | case .moreOrEqual(let value): 84 | return "more than or equal to \(value)" 85 | case .never: 86 | return "none" 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/Mockable/Models/GenericValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenericAttribute.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 17.. 6 | // 7 | 8 | /// A type erased wrapper for generic parameters. 9 | /// 10 | /// `GenericValue` encapsulates an arbitrary generic value along with a custom comparator closure. 11 | /// The comparator is used to determine equality between two instances of `GenericValue`. 12 | public struct GenericValue { 13 | /// The encapsulated value of type `Any`. 14 | public let value: Any 15 | 16 | /// The comparator closure used to determine equality between two instances of `GenericValue`. 17 | public let comparator: (Any, Any) -> Bool 18 | 19 | /// Initializes a new instance of `GenericValue`. 20 | /// 21 | /// - Parameters: 22 | /// - value: The value to be encapsulated. 23 | /// - comparator: The closure used to determine equality between two instances of `GenericValue`. 24 | public init(value: Any, comparator: @escaping (Any, Any) -> Bool) { 25 | self.value = value 26 | self.comparator = comparator 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Mockable/Models/Parameter+Match.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Parameter+Match.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 25.. 6 | // 7 | 8 | extension Parameter { 9 | private func match(_ parameter: Parameter, using comparator: Matcher.Comparator?) -> Bool { 10 | switch (self, parameter) { 11 | case (.any, _): return true 12 | case (_, .any): return true 13 | case (.value(let value), .matching(let matcher)): return matcher(value) 14 | case (.matching(let matcher), .value(let value)): return matcher(value) 15 | case (.value(let value1), .value(let value2)): 16 | guard let comparator else { 17 | fatalError(noComparatorMessage) 18 | } 19 | return comparator(value1, value2) 20 | default: return false 21 | } 22 | } 23 | } 24 | 25 | // MARK: - Default 26 | 27 | extension Parameter { 28 | /// Matches the current parameter with another parameter. 29 | /// 30 | /// - Parameter parameter: The parameter to match against. 31 | /// - Returns: `true` if the parameters match; otherwise, `false`. 32 | public func match(_ parameter: Parameter) -> Bool { 33 | match(parameter, using: Matcher.comparator(for: Value.self)) 34 | } 35 | } 36 | 37 | // MARK: - Equatable 38 | 39 | extension Parameter where Value: Equatable { 40 | /// Matches the current parameter with another parameter. 41 | /// 42 | /// - Parameter parameter: The parameter to match against. 43 | /// - Returns: `true` if the parameters match; otherwise, `false`. 44 | public func match(_ parameter: Parameter) -> Bool { 45 | match(parameter, using: Matcher.comparator(for: Value.self)) 46 | } 47 | } 48 | 49 | // MARK: - Sequence 50 | 51 | extension Parameter where Value: Sequence { 52 | /// Matches the current parameter with another parameter. 53 | /// 54 | /// - Parameter parameter: The parameter to match against. 55 | /// - Returns: `true` if the parameters match; otherwise, `false`. 56 | public func match(_ parameter: Parameter) -> Bool { 57 | match(parameter, using: Matcher.comparator(for: Value.self)) 58 | } 59 | } 60 | 61 | // MARK: - Equatable Sequence 62 | 63 | extension Parameter where Value: Equatable, Value: Sequence { 64 | /// Matches the current parameter with another parameter. 65 | /// 66 | /// - Parameter parameter: The parameter to match against. 67 | /// - Returns: `true` if the parameters match; otherwise, `false`. 68 | public func match(_ parameter: Parameter) -> Bool { 69 | match(parameter, using: Matcher.comparator(for: Value.self)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Mockable/Models/Parameter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Parameter.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 13.. 6 | // 7 | 8 | /// An enumeration representing different types of parameters used in mocking. 9 | public enum Parameter: @unchecked Sendable { 10 | /// Matches any value. 11 | case any 12 | /// Matches a specific value. 13 | case value(Value) 14 | /// Matches a value using a custom matching closure. 15 | case matching((Value) -> Bool) 16 | } 17 | 18 | extension Parameter { 19 | /// Creates a type erased parameter from a value of type `T`. 20 | /// 21 | /// - Parameter value: The value to be encapsulated in a generic parameter. 22 | /// - Returns: A a type erased parameter containing the provided value. 23 | public static func generic(_ value: T) -> Parameter { 24 | Parameter.value(value).eraseToGenericValue() 25 | } 26 | 27 | /// Type erases a parameter of type `Parameter` to a `Parameter` 28 | /// 29 | /// - Returns: A type erased parameter with the same matching behavior. 30 | public func eraseToGenericValue() -> Parameter { 31 | switch self { 32 | case .any: 33 | return .any 34 | case .value(let value): 35 | let value = GenericValue(value: value) { left, right in 36 | guard let left = left as? Value, 37 | let right = right as? Value else { return false } 38 | 39 | guard let comparator = Matcher.comparator(for: Value.self) else { 40 | fatalError(noComparatorMessage) 41 | } 42 | return comparator(left, right) 43 | } 44 | return .value(value) 45 | case .matching(let matcher): 46 | return .matching { value in 47 | guard let value = value.value as? Value else { return false } 48 | return matcher(value) 49 | } 50 | } 51 | } 52 | } 53 | 54 | extension Parameter { 55 | var noComparatorMessage: String { 56 | """ 57 | No comparator found for type "\(Value.self)". \ 58 | All non-equatable types must be registered using Matcher.register(_). 59 | """ 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/Mockable/Models/ReturnValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReturnValue.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 13.. 6 | // 7 | 8 | /// An enumeration representing different types of return values for mocked functions. 9 | /// 10 | /// - `return(Any)`: A concrete value to be returned. 11 | /// - `throw(Error)`: An error to be thrown. 12 | /// - `produce(Any)`: A value producer to be invoked. 13 | public enum ReturnValue { 14 | /// A case representing a concrete value to be returned. 15 | case `return`(Any) 16 | 17 | /// A case representing an error to be thrown. 18 | case `throw`(any Error) 19 | 20 | /// A case representing a value producer to be invoked. 21 | case produce(Any) 22 | } 23 | 24 | /// An error thrown when type erased producers cannot be casted to their original closure type. 25 | public enum ProducerCastError: Error { 26 | case typeMismatch 27 | } 28 | 29 | /// Casts the given producer to the specified type. 30 | /// 31 | /// This function is used to cast the producer to a specific type when using the `.produce` case 32 | /// in the `ReturnValue` enum. 33 | /// 34 | /// - Parameter producer: The producer to be cast. 35 | /// - Returns: The casted producer of type `P`. 36 | public func cast

(_ producer: Any) throws -> P { 37 | guard let producer = producer as? P else { 38 | throw ProducerCastError.typeMismatch 39 | } 40 | return producer 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Mockable/Models/TimeoutDuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeoutDuration.swift 3 | // Mockable 4 | // 5 | // 6 | // Created by Nayanda Haberty on 3/4/24. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An enumeration representing a duration of time. 12 | public enum TimeoutDuration { 13 | 14 | /// A TimeoutDuration representing a given number of seconds. 15 | case seconds(Double) 16 | 17 | /// A TimeoutDuration representing a given number of miliseconds. 18 | case miliseconds(UInt) 19 | 20 | /// Converts the duration to TimeInterval. 21 | public var duration: TimeInterval { 22 | switch self { 23 | case .seconds(let value): value 24 | case .miliseconds(let value): TimeInterval(value) / 1000 25 | } 26 | } 27 | } 28 | 29 | // MARK: - ExpressibleByFloatLiteral 30 | 31 | extension TimeoutDuration: ExpressibleByFloatLiteral { 32 | public init(floatLiteral value: Double) { 33 | self = .seconds(value) 34 | } 35 | } 36 | 37 | // MARK: - ExpressibleByIntegerLiteral 38 | 39 | extension TimeoutDuration: ExpressibleByIntegerLiteral { 40 | public init(integerLiteral value: Int) { 41 | self = .seconds(Double(value)) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Mockable/Utils/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 17.. 6 | // 7 | 8 | /// Creates a proxy for building return values for members of the given service. 9 | /// 10 | /// Example usage of `given(_ service:)`: 11 | /// ```swift 12 | /// // Throw an error for the first call and then return 'product' for every other call 13 | /// given(productService) 14 | /// .fetch(for: .any).willThrow(error) 15 | /// .fetch(for: .any).willReturn(product) 16 | /// 17 | /// // Throw an error if the id parameter ends with a 0, return a product otherwise 18 | /// given(productService) 19 | /// .fetch(for: .any).willProduce { id in 20 | /// if id.uuidString.last == "0" { 21 | /// throw error 22 | /// } else { 23 | /// return product 24 | /// } 25 | /// } 26 | /// ``` 27 | /// 28 | /// - Parameter service: The mockable service for which return values are specified. 29 | /// - Returns: The service's return value builder. 30 | public func given(_ service: T) -> T.ReturnBuilder { service._given } 31 | 32 | /// Creates a proxy for building actions for members of the given service. 33 | /// 34 | /// Example usage of `when(_ service:)`: 35 | /// ```swift 36 | /// // log calls to fetch(for:) 37 | /// when(productService).fetch(for: .any).perform { 38 | /// print("fetch(for:) was called") 39 | /// } 40 | /// 41 | /// // log when url is accessed 42 | /// when(productService).url().performOnGet { 43 | /// print("url accessed") 44 | /// } 45 | /// 46 | /// // log when url is set to nil 47 | /// when(productService).url(newValue: .value(nil)).performOnSet { 48 | /// print("url set to nil") 49 | /// } 50 | /// ``` 51 | /// 52 | /// - Parameter service: The mockable service for which actions are specified. 53 | /// - Returns: The service's action builder. 54 | public func when(_ service: T) -> T.ActionBuilder { service._when } 55 | 56 | /// Creates a proxy for verifying invocations of members of the given service. 57 | /// 58 | /// Example usage of `verify(_ service:)`: 59 | /// ```swift 60 | /// verify(productService) 61 | /// // assert fetch(for:) was called between 1 and 5 times 62 | /// .fetch(for: .any).called(.from(1, to: 5)) 63 | /// // assert checkout(with:) was called between exactly 10 times 64 | /// .checkout(with: .any).called(10) 65 | /// // assert url property was accessed at least 2 times 66 | /// .url().getCalled(.moreOrEqual(to: 2)) 67 | /// // assert url property was never set to nil 68 | /// .url(newValue: .value(nil)).setCalled(.never) 69 | /// ``` 70 | /// - Parameter service: The mockable service for which invocations are verified. 71 | /// - Returns: The service's verification builder. 72 | public func verify(_ service: T) -> T.VerifyBuilder { service._verify } 73 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Extensions/AttributeSyntax+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributeSyntax+Extensions.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 25.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | extension AttributeSyntax { 11 | static var escaping: Self { 12 | AttributeSyntax( 13 | atSign: .atSignToken(), 14 | attributeName: IdentifierTypeSyntax( 15 | name: .keyword(.escaping) 16 | ) 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Extensions/DeclModifierListSyntax+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeclModifierListSyntax+Extensions.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 27/09/2024. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | extension DeclModifierListSyntax { 11 | func filtered(keywords: Set) -> DeclModifierListSyntax { 12 | filter { modifier in 13 | guard case .keyword(let keyword) = modifier.name.tokenKind else { return false } 14 | return keywords.contains(keyword) 15 | } 16 | } 17 | 18 | func appending(_ other: DeclModifierListSyntax) -> DeclModifierListSyntax { 19 | self + Array(other) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Extensions/FunctionDeclSyntax+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FunctionDeclSyntax+Extensions.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 23.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | extension FunctionDeclSyntax { 11 | var isVoid: Bool { 12 | signature.returnClause == nil 13 | } 14 | 15 | var isThrowing: Bool { 16 | #if canImport(SwiftSyntax600) 17 | signature.effectSpecifiers?.throwsClause?.throwsSpecifier.tokenKind == .keyword(.throws) 18 | #else 19 | signature.effectSpecifiers?.throwsSpecifier?.tokenKind == .keyword(.throws) 20 | #endif 21 | } 22 | 23 | var closureType: FunctionTypeSyntax { 24 | let params = signature.parameterClause.parameters 25 | .map { $0.resolvedType(for: .parameter) } 26 | 27 | #if canImport(SwiftSyntax600) 28 | let effectSpecifiers = TypeEffectSpecifiersSyntax( 29 | throwsClause: isThrowing ? .init(throwsSpecifier: .keyword(.throws)) : nil 30 | ) 31 | #else 32 | let effectSpecifiers = TypeEffectSpecifiersSyntax( 33 | throwsSpecifier: isThrowing ? .keyword(.throws) : nil 34 | ) 35 | #endif 36 | return FunctionTypeSyntax( 37 | parameters: TupleTypeElementListSyntax { 38 | for param in params { 39 | TupleTypeElementSyntax(type: param) 40 | } 41 | }, 42 | effectSpecifiers: effectSpecifiers, 43 | returnClause: signature.returnClause ?? .init(type: IdentifierTypeSyntax(name: NS.Void)) 44 | ) 45 | } 46 | 47 | func containsGenericType(in functionParameter: FunctionParameterSyntax) -> Bool { 48 | let genericParameters = genericParameterClause?.parameters ?? [] 49 | guard !genericParameters.isEmpty else { return false } 50 | let generics = genericParameters.map(\.name.trimmedDescription) 51 | return nil != TokenFinder.find(in: functionParameter.type) { 52 | guard case .identifier(let name) = $0.tokenKind else { return false } 53 | return generics.contains(name) 54 | } 55 | } 56 | } 57 | 58 | #if canImport(SwiftSyntax600) 59 | extension FunctionDeclSyntax { 60 | var errorType: TypeSyntax? { 61 | signature.effectSpecifiers?.throwsClause?.type 62 | } 63 | } 64 | #endif 65 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Extensions/FunctionParameterSyntax+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FunctionParameterSyntax+Extensions.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 20.. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftSyntaxMacros 10 | 11 | enum FunctionParameterTypeResolveRole { 12 | /// Resolves a type that can be used for property type bindings, removes attributes. 13 | case binding 14 | /// Resolves a type that can be used as a function or closure parameter, keeps attributes. 15 | case parameter 16 | } 17 | 18 | extension FunctionParameterSyntax { 19 | func resolvedType(for role: FunctionParameterTypeResolveRole = .binding) -> TypeSyntax { 20 | guard ellipsis == nil else { 21 | return TypeSyntax(ArrayTypeSyntax(element: type)) 22 | } 23 | 24 | guard role != .parameter else { return type } 25 | 26 | if let attributeType = type.as(AttributedTypeSyntax.self) { 27 | return TypeSyntax(attributeType.baseType) 28 | } else { 29 | return TypeSyntax(type) 30 | } 31 | } 32 | 33 | var isInout: Bool { 34 | #if canImport(SwiftSyntax600) 35 | type.as(AttributedTypeSyntax.self)?.specifiers.contains { specifier in 36 | guard case .simpleTypeSpecifier(let simpleSpecifier) = specifier else { return false } 37 | return simpleSpecifier.specifier.tokenKind == .keyword(.inout) 38 | } ?? false 39 | #else 40 | type.as(AttributedTypeSyntax.self)?.specifier?.tokenKind == .keyword(.inout) 41 | #endif 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Extensions/ProtocolDeclSyntax+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProtocolDeclSyntax+Extensions.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 23.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | extension ProtocolDeclSyntax { 11 | var mockName: String { 12 | NS.Mock.trimmedDescription + name.trimmedDescription 13 | } 14 | 15 | var mockType: IdentifierTypeSyntax { 16 | IdentifierTypeSyntax(name: .identifier(mockName)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 19.. 6 | // 7 | 8 | extension String { 9 | var capitalizedFirstLetter: String { 10 | let firstLetter = prefix(1).capitalized 11 | let remainingLetters = dropFirst() 12 | return firstLetter + remainingLetters 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Extensions/TokenSyntax+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenSyntax+Extensions.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltányi on 2025. 05. 06.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | extension TokenSyntax { 11 | public var declNameOrVarCallName: Self { 12 | let text = trimmed.description 13 | if text.hasPrefix("`") && text.hasSuffix("`") { 14 | return self 15 | } 16 | if Keyword.all.contains(text) { 17 | return "`\(raw: text)`" 18 | } else { 19 | return self 20 | } 21 | } 22 | } 23 | 24 | extension Keyword { 25 | fileprivate static let all: [String] = [ 26 | "__consuming", 27 | "__owned", 28 | "__setter_access", 29 | "__shared", 30 | "_alignment", 31 | "_backDeploy", 32 | "_borrow", 33 | "_borrowing", 34 | "_BridgeObject", 35 | "_cdecl", 36 | "_Class", 37 | "_compilerInitialized", 38 | "_const", 39 | "_consuming", 40 | "_documentation", 41 | "_dynamicReplacement", 42 | "_effects", 43 | "_expose", 44 | "_forward", 45 | "_implements", 46 | "_linear", 47 | "_local", 48 | "_modify", 49 | "_move", 50 | "_mutating", 51 | "_NativeClass", 52 | "_NativeRefCountedObject", 53 | "_noMetadata", 54 | "_nonSendable", 55 | "_objcImplementation", 56 | "_objcRuntimeName", 57 | "_opaqueReturnTypeOf", 58 | "_optimize", 59 | "_originallyDefinedIn", 60 | "_PackageDescription", 61 | "_private", 62 | "_projectedValueProperty", 63 | "_read", 64 | "_RefCountedObject", 65 | "_semantics", 66 | "_specialize", 67 | "_spi", 68 | "_spi_available", 69 | "_swift_native_objc_runtime_base", 70 | "_Trivial", 71 | "_TrivialAtMost", 72 | "_TrivialStride", 73 | "_typeEraser", 74 | "_unavailableFromAsync", 75 | "_underlyingVersion", 76 | "_UnknownLayout", 77 | "_version", 78 | "accesses", 79 | "actor", 80 | "addressWithNativeOwner", 81 | "addressWithOwner", 82 | "any", 83 | "Any", 84 | "as", 85 | "assignment", 86 | "associatedtype", 87 | "associativity", 88 | "async", 89 | "attached", 90 | "autoclosure", 91 | "availability", 92 | "available", 93 | "await", 94 | "backDeployed", 95 | "before", 96 | "block", 97 | "borrowing", 98 | "break", 99 | "canImport", 100 | "case", 101 | "catch", 102 | "class", 103 | "compiler", 104 | "consume", 105 | "copy", 106 | "consuming", 107 | "continue", 108 | "convenience", 109 | "convention", 110 | "cType", 111 | "default", 112 | "defer", 113 | "deinit", 114 | "dependsOn", 115 | "deprecated", 116 | "derivative", 117 | "didSet", 118 | "differentiable", 119 | "distributed", 120 | "do", 121 | "dynamic", 122 | "each", 123 | "else", 124 | "enum", 125 | "escaping", 126 | "exclusivity", 127 | "exported", 128 | "extension", 129 | "fallthrough", 130 | "false", 131 | "file", 132 | "fileprivate", 133 | "final", 134 | "for", 135 | "discard", 136 | "forward", 137 | "func", 138 | "freestanding", 139 | "get", 140 | "guard", 141 | "higherThan", 142 | "if", 143 | "import", 144 | "in", 145 | "indirect", 146 | "infix", 147 | "init", 148 | "initializes", 149 | "inline", 150 | "inout", 151 | "internal", 152 | "introduced", 153 | "is", 154 | "isolated", 155 | "kind", 156 | "lazy", 157 | "left", 158 | "let", 159 | "line", 160 | "linear", 161 | "lowerThan", 162 | "macro", 163 | "message", 164 | "metadata", 165 | "module", 166 | "mutableAddressWithNativeOwner", 167 | "mutableAddressWithOwner", 168 | "mutating", 169 | "nil", 170 | "noasync", 171 | "noDerivative", 172 | "noescape", 173 | "none", 174 | "nonisolated", 175 | "nonmutating", 176 | "objc", 177 | "obsoleted", 178 | "of", 179 | "open", 180 | "operator", 181 | "optional", 182 | "override", 183 | "package", 184 | "postfix", 185 | "precedencegroup", 186 | "preconcurrency", 187 | "prefix", 188 | "private", 189 | "Protocol", 190 | "protocol", 191 | "public", 192 | "reasync", 193 | "renamed", 194 | "repeat", 195 | "required", 196 | "_resultDependsOn", 197 | "_resultDependsOnSelf", 198 | "rethrows", 199 | "retroactive", 200 | "return", 201 | "reverse", 202 | "right", 203 | "safe", 204 | "scoped", 205 | "self", 206 | "sending", 207 | "Self", 208 | "Sendable", 209 | "set", 210 | "some", 211 | "sourceFile", 212 | "spi", 213 | "spiModule", 214 | "static", 215 | "struct", 216 | "subscript", 217 | "super", 218 | "swift", 219 | "switch", 220 | "target", 221 | "then", 222 | "throw", 223 | "throws", 224 | "transpose", 225 | "true", 226 | "try", 227 | "Type", 228 | "typealias", 229 | "unavailable", 230 | "unchecked", 231 | "unowned", 232 | "unsafe", 233 | "unsafeAddress", 234 | "unsafeMutableAddress", 235 | "var", 236 | "visibility", 237 | "weak", 238 | "where", 239 | "while", 240 | "willSet", 241 | "witness_method", 242 | "wrt", 243 | "yield" 244 | ] 245 | } 246 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Extensions/VariableDeclSyntax+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VariableDeclSyntax+Extensions.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 10.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | extension VariableDeclSyntax { 11 | var name: TokenSyntax { 12 | get throws { 13 | .identifier(try binding.pattern.trimmedDescription) 14 | } 15 | } 16 | 17 | var isComputed: Bool { setAccessor == nil } 18 | 19 | var isThrowing: Bool { 20 | get throws { 21 | #if canImport(SwiftSyntax600) 22 | try getAccessor.effectSpecifiers?.throwsClause?.throwsSpecifier != nil 23 | #else 24 | try getAccessor.effectSpecifiers?.throwsSpecifier != nil 25 | #endif 26 | } 27 | } 28 | 29 | var resolvedType: TypeSyntax { 30 | get throws { 31 | let type = try type 32 | if let type = type.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) { 33 | return type.wrappedType 34 | } 35 | return type 36 | } 37 | } 38 | 39 | var type: TypeSyntax { 40 | get throws { 41 | guard let typeAnnotation = try binding.typeAnnotation else { 42 | throw MockableMacroError.invalidVariableRequirement 43 | } 44 | return typeAnnotation.type.trimmed 45 | } 46 | } 47 | 48 | var getAccessor: AccessorDeclSyntax { 49 | get throws { 50 | let getAccessor = try accessors.first { $0.accessorSpecifier.tokenKind == .keyword(.get) } 51 | guard let getAccessor else { throw MockableMacroError.invalidVariableRequirement } 52 | return getAccessor 53 | } 54 | } 55 | 56 | var setAccessor: AccessorDeclSyntax? { 57 | try? accessors.first { $0.accessorSpecifier.tokenKind == .keyword(.set) } 58 | } 59 | 60 | var closureType: FunctionTypeSyntax { 61 | get throws { 62 | #if canImport(SwiftSyntax600) 63 | let effectSpecifiers = TypeEffectSpecifiersSyntax( 64 | throwsClause: try isThrowing ? .init(throwsSpecifier: .keyword(.throws)) : nil 65 | ) 66 | #else 67 | let effectSpecifiers = TypeEffectSpecifiersSyntax( 68 | throwsSpecifier: try isThrowing ? .keyword(.throws) : nil 69 | ) 70 | #endif 71 | return FunctionTypeSyntax( 72 | parameters: TupleTypeElementListSyntax(), 73 | effectSpecifiers: effectSpecifiers, 74 | returnClause: .init(type: try resolvedType) 75 | ) 76 | } 77 | } 78 | 79 | var binding: PatternBindingSyntax { 80 | get throws { 81 | guard let binding = bindings.first else { 82 | throw MockableMacroError.invalidVariableRequirement 83 | } 84 | return binding 85 | } 86 | } 87 | } 88 | 89 | // MARK: - Helpers 90 | 91 | extension VariableDeclSyntax { 92 | private var accessors: AccessorDeclListSyntax { 93 | get throws { 94 | guard let accessorBlock = try binding.accessorBlock, 95 | case .accessors(let accessorList) = accessorBlock.accessors else { 96 | throw MockableMacroError.invalidVariableRequirement 97 | } 98 | return accessorList 99 | } 100 | } 101 | } 102 | 103 | #if canImport(SwiftSyntax600) 104 | extension VariableDeclSyntax { 105 | var errorType: TypeSyntax? { 106 | get throws { try getAccessor.effectSpecifiers?.throwsClause?.type } 107 | } 108 | } 109 | #endif 110 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Factory/Buildable/Buildable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Buildable.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 23.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | /// An enum representing the different builder structs to generate. 11 | enum BuilderKind: CaseIterable { 12 | case `return` 13 | case action 14 | case verify 15 | 16 | /// The builder struct declaration's name. 17 | var name: TokenSyntax { 18 | switch self { 19 | case .return: NS.ReturnBuilder 20 | case .action: NS.ActionBuilder 21 | case .verify: NS.VerifyBuilder 22 | } 23 | } 24 | 25 | /// The builder struct declaration's type. 26 | var type: IdentifierTypeSyntax { 27 | IdentifierTypeSyntax(name: name) 28 | } 29 | } 30 | 31 | /// A protocol to associate builder functions with individual requirements. 32 | /// 33 | /// Used to generate members of builder struct declarations in builder factory. 34 | protocol Buildable { 35 | /// Returns the specified builder implementation. 36 | /// 37 | /// - Parameter kind: The kind of builder function to generate. 38 | /// - Parameter modifiers: Declaration modifiers to add to the builder. 39 | /// - Parameter mockType: The enclosing mock service's type. 40 | /// - Returns: A function or variable declaration that mirrors a protocol requirement 41 | /// with its syntax. The parameters of the generated declaration are wrapped in the `Parameter` 42 | /// wrapper, and it returns the corresponding builder object. 43 | func builder( 44 | of kind: BuilderKind, 45 | with modifiers: DeclModifierListSyntax, 46 | using mockType: IdentifierTypeSyntax 47 | ) throws -> DeclSyntax 48 | } 49 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Factory/Buildable/Function+Buildable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Function+Buildable.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 23.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | // MARK: - FunctionRequirement + Buildable 11 | 12 | extension FunctionRequirement: Buildable { 13 | func builder( 14 | of kind: BuilderKind, 15 | with modifiers: DeclModifierListSyntax, 16 | using mockType: IdentifierTypeSyntax 17 | ) throws -> DeclSyntax { 18 | let decl = FunctionDeclSyntax( 19 | attributes: syntax.attributes.trimmed.with(\.trailingTrivia, .newline), 20 | modifiers: modifiers, 21 | name: syntax.name.trimmed, 22 | genericParameterClause: genericParameterClause(for: kind), 23 | signature: signature(for: kind, using: mockType), 24 | genericWhereClause: genericWhereClause(for: kind), 25 | body: try body(for: kind) 26 | ) 27 | return DeclSyntax(decl) 28 | } 29 | } 30 | 31 | // MARK: - Helpers 32 | 33 | extension FunctionRequirement { 34 | private func genericParameterClause(for kind: BuilderKind) -> GenericParameterClauseSyntax? { 35 | switch kind { 36 | case .return: syntax.genericParameterClause 37 | case .action: filteredGenericParameterClause 38 | case .verify: filteredGenericParameterClause 39 | } 40 | } 41 | 42 | private func genericWhereClause(for kind: BuilderKind) -> GenericWhereClauseSyntax? { 43 | switch kind { 44 | case .return: syntax.genericWhereClause 45 | case .action: filteredGenericWhereClause 46 | case .verify: filteredGenericWhereClause 47 | } 48 | } 49 | 50 | private func signature( 51 | for kind: BuilderKind, 52 | using mockType: IdentifierTypeSyntax 53 | ) -> FunctionSignatureSyntax { 54 | FunctionSignatureSyntax( 55 | parameterClause: parameterClause, 56 | returnClause: returnClause(for: kind, using: mockType) 57 | ) 58 | } 59 | 60 | private var parameterClause: FunctionParameterClauseSyntax { 61 | let parameters = FunctionParameterListSyntax { 62 | for parameter in syntax.signature.parameterClause.parameters { 63 | FunctionParameterSyntax( 64 | firstName: parameter.firstName, 65 | secondName: parameter.secondName, 66 | type: IdentifierTypeSyntax(name: NS.Parameter(parameter.resolvedType().description)) 67 | ) 68 | } 69 | } 70 | return FunctionParameterClauseSyntax(parameters: parameters) 71 | } 72 | 73 | private func returnClause( 74 | for kind: BuilderKind, 75 | using mockType: IdentifierTypeSyntax 76 | ) -> ReturnClauseSyntax { 77 | let name = syntax.isThrowing ? NS.ThrowingFunction(kind) : NS.Function(kind) 78 | let arguments = GenericArgumentListSyntax { 79 | GenericArgumentSyntax(argument: mockType) 80 | GenericArgumentSyntax(argument: kind.type) 81 | if let returnType = functionReturnType(for: kind) { 82 | returnType 83 | } 84 | if let errorType = errorType(for: kind) { 85 | errorType 86 | } 87 | if let produceType = functionProduceType(for: kind) { 88 | produceType 89 | } 90 | } 91 | return ReturnClauseSyntax( 92 | type: MemberTypeSyntax( 93 | baseType: IdentifierTypeSyntax(name: NS.Mockable), 94 | name: name, 95 | genericArgumentClause: .init(arguments: arguments) 96 | ) 97 | ) 98 | } 99 | 100 | private func errorType(for kind: BuilderKind) -> GenericArgumentSyntax? { 101 | guard syntax.isThrowing && kind == .return else { return nil } 102 | #if canImport(SwiftSyntax600) 103 | guard let errorType = syntax.errorType else { 104 | return GenericArgumentSyntax(argument: defaultErrorType) 105 | } 106 | return GenericArgumentSyntax(argument: errorType.trimmed) 107 | #else 108 | return GenericArgumentSyntax(argument: defaultErrorType) 109 | #endif 110 | } 111 | 112 | private var defaultErrorType: some TypeSyntaxProtocol { 113 | SomeOrAnyTypeSyntax( 114 | someOrAnySpecifier: .keyword(.any), 115 | constraint: IdentifierTypeSyntax(name: NS.Error) 116 | ) 117 | } 118 | 119 | private func functionReturnType(for kind: BuilderKind) -> GenericArgumentSyntax? { 120 | guard kind == .return else { return nil } 121 | guard let returnClause = syntax.signature.returnClause else { 122 | let voidType = IdentifierTypeSyntax(name: NS.Void) 123 | return GenericArgumentSyntax(argument: voidType) 124 | } 125 | return GenericArgumentSyntax(argument: returnClause.type) 126 | } 127 | 128 | private func functionProduceType(for kind: BuilderKind) -> GenericArgumentSyntax? { 129 | guard kind == .return else { return nil } 130 | return GenericArgumentSyntax(argument: syntax.closureType) 131 | } 132 | 133 | private func body(for kind: BuilderKind) throws -> CodeBlockSyntax { 134 | let arguments = try LabeledExprListSyntax { 135 | LabeledExprSyntax( 136 | expression: DeclReferenceExprSyntax(baseName: NS.mocker) 137 | ) 138 | LabeledExprSyntax( 139 | label: NS.kind, 140 | colon: .colonToken(), 141 | expression: try caseSpecifier(wrapParams: false) 142 | ) 143 | } 144 | let statements = CodeBlockItemListSyntax { 145 | FunctionCallExprSyntax( 146 | calledExpression: MemberAccessExprSyntax(name: NS.initializer), 147 | leftParen: .leftParenToken(), 148 | arguments: arguments, 149 | rightParen: .rightParenToken() 150 | ) 151 | } 152 | return .init(statements: statements) 153 | } 154 | 155 | /// Returns the function's generic parameter clause with return only generics filtered out. 156 | /// If a generic parameter is only used in the return clause of a function, it will not 157 | /// be part of the returned generic parameter clause. 158 | private var filteredGenericParameterClause: GenericParameterClauseSyntax? { 159 | guard let generics = syntax.genericParameterClause else { return nil } 160 | var parameters = generics.parameters.filter { generic in 161 | hasParameter(containing: generic.name.trimmedDescription) 162 | } 163 | if let lastIndex = parameters.indices.last { 164 | parameters[lastIndex] = parameters[lastIndex].with(\.trailingComma, nil) 165 | } 166 | return parameters.isEmpty ? nil : .init(parameters: parameters) 167 | } 168 | 169 | /// Returns the function's generic where clause with return only generics filtered out. 170 | /// If a generic parameter is only used in the return clause of a function, it will not 171 | /// be part of the returned generic where clause. 172 | private var filteredGenericWhereClause: GenericWhereClauseSyntax? { 173 | guard let generics = syntax.genericWhereClause else { return nil } 174 | var requirements = generics.requirements.filter { requirement in 175 | switch requirement.requirement { 176 | case .conformanceRequirement(let conformance): 177 | return hasParameter(containing: conformance.leftType.trimmedDescription) 178 | case .sameTypeRequirement(let sameType): 179 | return hasParameter(containing: sameType.leftType.trimmedDescription) 180 | default: 181 | return false 182 | } 183 | } 184 | if let lastIndex = requirements.indices.last { 185 | var last = requirements[lastIndex] 186 | last.trailingComma = nil 187 | requirements[lastIndex] = last 188 | } 189 | let whereClause = GenericWhereClauseSyntax(requirements: requirements) 190 | .with(\.trailingTrivia, .space) 191 | return requirements.isEmpty ? nil : whereClause 192 | } 193 | 194 | private func hasParameter(containing identifier: String) -> Bool { 195 | nil != TokenFinder.find(in: syntax.signature.parameterClause) { 196 | $0.tokenKind == .identifier(identifier) 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Factory/Buildable/Variable+Buildable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Variable+Buildable.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 23.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | // MARK: - VariableRequirement + Buildable 11 | 12 | extension VariableRequirement: Buildable { 13 | func builder( 14 | of kind: BuilderKind, 15 | with modifiers: DeclModifierListSyntax, 16 | using mockType: IdentifierTypeSyntax 17 | ) throws -> DeclSyntax { 18 | if syntax.isComputed || kind == .return { 19 | return try variableDeclaration(of: kind, with: modifiers, using: mockType) 20 | } else { 21 | return try functionDeclaration(of: kind, with: modifiers, using: mockType) 22 | } 23 | } 24 | } 25 | 26 | // MARK: - Helpers 27 | 28 | extension VariableRequirement { 29 | func variableDeclaration( 30 | of kind: BuilderKind, 31 | with modifiers: DeclModifierListSyntax, 32 | using mockType: IdentifierTypeSyntax 33 | ) throws -> DeclSyntax { 34 | let variableDecl = VariableDeclSyntax( 35 | attributes: syntax.attributes.trimmed.with(\.trailingTrivia, .newline), 36 | modifiers: modifiers, 37 | bindingSpecifier: .keyword(.var), 38 | bindings: try PatternBindingListSyntax { 39 | PatternBindingSyntax( 40 | pattern: try syntax.binding.pattern, 41 | typeAnnotation: TypeAnnotationSyntax( 42 | type: try returnType(for: kind, using: mockType) 43 | ), 44 | accessorBlock: AccessorBlockSyntax( 45 | accessors: .getter(try body(for: kind)) 46 | ) 47 | ) 48 | } 49 | ) 50 | return DeclSyntax(variableDecl) 51 | } 52 | 53 | private func functionDeclaration( 54 | of kind: BuilderKind, 55 | with modifiers: DeclModifierListSyntax, 56 | using mockType: IdentifierTypeSyntax 57 | ) throws -> DeclSyntax { 58 | let functionDecl = FunctionDeclSyntax( 59 | attributes: syntax.attributes.trimmed.with(\.trailingTrivia, .newline), 60 | modifiers: modifiers, 61 | name: try syntax.name, 62 | signature: try signature(for: kind, using: mockType), 63 | body: .init(statements: try body(for: kind)) 64 | ) 65 | return DeclSyntax(functionDecl) 66 | } 67 | 68 | private func signature( 69 | for kind: BuilderKind, 70 | using mockType: IdentifierTypeSyntax 71 | ) throws -> FunctionSignatureSyntax { 72 | let parameters = try FunctionParameterListSyntax { 73 | FunctionParameterSyntax( 74 | firstName: NS.newValue, 75 | type: IdentifierTypeSyntax( 76 | name: NS.Parameter(try syntax.resolvedType.trimmedDescription) 77 | ), 78 | defaultValue: InitializerClauseSyntax( 79 | value: MemberAccessExprSyntax(name: NS.any) 80 | ) 81 | ) 82 | } 83 | return FunctionSignatureSyntax( 84 | parameterClause: FunctionParameterClauseSyntax(parameters: parameters), 85 | returnClause: ReturnClauseSyntax( 86 | type: try returnType(for: kind, using: mockType) 87 | ) 88 | ) 89 | } 90 | 91 | private func returnType( 92 | for kind: BuilderKind, 93 | using mockType: IdentifierTypeSyntax 94 | ) throws -> MemberTypeSyntax { 95 | let name = if syntax.isComputed { 96 | try syntax.isThrowing ? NS.ThrowingFunction(kind) : NS.Function(kind) 97 | } else { 98 | try syntax.isThrowing ? NS.ThrowingProperty(kind) : NS.Property(kind) 99 | } 100 | 101 | let arguments = try GenericArgumentListSyntax { 102 | GenericArgumentSyntax(argument: mockType) 103 | GenericArgumentSyntax(argument: kind.type) 104 | if let returnType = try variableReturnType(for: kind) { 105 | returnType 106 | } 107 | if let errorType = try errorType(for: kind) { 108 | errorType 109 | } 110 | if let produceType = try variableProduceType(for: kind) { 111 | produceType 112 | } 113 | } 114 | 115 | return MemberTypeSyntax( 116 | baseType: IdentifierTypeSyntax(name: NS.Mockable), 117 | name: name, 118 | genericArgumentClause: .init(arguments: arguments) 119 | ) 120 | } 121 | 122 | private func errorType(for kind: BuilderKind) throws -> GenericArgumentSyntax? { 123 | guard try syntax.isThrowing && syntax.isComputed && kind == .return else { return nil } 124 | #if canImport(SwiftSyntax600) 125 | guard let errorType = try syntax.errorType else { 126 | return GenericArgumentSyntax(argument: defaultErrorType) 127 | } 128 | return GenericArgumentSyntax(argument: errorType.trimmed) 129 | #else 130 | return GenericArgumentSyntax(argument: defaultErrorType) 131 | #endif 132 | } 133 | 134 | private var defaultErrorType: some TypeSyntaxProtocol { 135 | SomeOrAnyTypeSyntax( 136 | someOrAnySpecifier: .keyword(.any), 137 | constraint: IdentifierTypeSyntax(name: NS.Error) 138 | ) 139 | } 140 | 141 | private func variableReturnType(for kind: BuilderKind) throws -> GenericArgumentSyntax? { 142 | guard kind == .return else { return nil } 143 | return GenericArgumentSyntax(argument: try syntax.resolvedType) 144 | } 145 | 146 | private func variableProduceType(for kind: BuilderKind) throws -> GenericArgumentSyntax? { 147 | guard kind == .return, syntax.isComputed else { return nil } 148 | return GenericArgumentSyntax(argument: try syntax.closureType) 149 | } 150 | 151 | private func body(for kind: BuilderKind) throws -> CodeBlockItemListSyntax { 152 | let arguments = try LabeledExprListSyntax { 153 | LabeledExprSyntax( 154 | expression: DeclReferenceExprSyntax(baseName: NS.mocker) 155 | ) 156 | LabeledExprSyntax( 157 | label: NS.kind, 158 | colon: .colonToken(), 159 | expression: try caseSpecifier(wrapParams: false) 160 | ) 161 | if kind != .return, let setterSpecifier = try setterCaseSpecifier(wrapParams: false) { 162 | LabeledExprSyntax( 163 | label: NS.setKind, 164 | colon: .colonToken(), 165 | expression: setterSpecifier 166 | ) 167 | } 168 | } 169 | return CodeBlockItemListSyntax { 170 | FunctionCallExprSyntax( 171 | calledExpression: MemberAccessExprSyntax(name: NS._init), 172 | leftParen: .leftParenToken(), 173 | arguments: arguments, 174 | rightParen: .rightParenToken() 175 | ) 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Factory/BuilderFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuilderFactory.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 23.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | /// Factory to generate builder struct declarations. 11 | /// 12 | /// Creates a member block item list that includes `ReturnBuilder`, 13 | /// `ActionBuilder` and `VerifyBuilder` struct declarations. 14 | enum BuilderFactory: Factory { 15 | static func build(from requirements: Requirements) throws -> MemberBlockItemListSyntax { 16 | try MemberBlockItemListSyntax { 17 | for builder in BuilderKind.allCases { 18 | try builderDeclaration(for: builder, requirements) 19 | } 20 | } 21 | } 22 | } 23 | 24 | // MARK: - Helpers 25 | 26 | extension BuilderFactory { 27 | private static func builderDeclaration( 28 | for kind: BuilderKind, 29 | _ requirements: Requirements 30 | ) throws -> some DeclSyntaxProtocol { 31 | StructDeclSyntax( 32 | modifiers: requirements.modifiers, 33 | name: kind.name, 34 | inheritanceClause: InheritanceClauseSyntax( 35 | inheritedTypes: [ 36 | InheritedTypeSyntax( 37 | type: MemberTypeSyntax( 38 | baseType: IdentifierTypeSyntax(name: NS.Mockable), 39 | name: NS.Builder 40 | ) 41 | ) 42 | ] 43 | ), 44 | memberBlock: MemberBlockSyntax(members: try members(kind, requirements)) 45 | ) 46 | } 47 | 48 | private static func members(_ kind: BuilderKind, _ requirements: Requirements) throws -> MemberBlockItemListSyntax { 49 | try MemberBlockItemListSyntax { 50 | mockerDeclaration(requirements) 51 | initializerDeclaration(kind, requirements) 52 | for variable in requirements.variables { 53 | MemberBlockItemSyntax( 54 | decl: try variable.builder( 55 | of: kind, 56 | with: requirements.modifiers, 57 | using: requirements.syntax.mockType 58 | ) 59 | ) 60 | } 61 | for function in requirements.functions { 62 | MemberBlockItemSyntax( 63 | decl: try function.builder( 64 | of: kind, 65 | with: requirements.modifiers, 66 | using: requirements.syntax.mockType 67 | ) 68 | ) 69 | } 70 | } 71 | } 72 | 73 | private static func mockerDeclaration(_ requirements: Requirements) -> VariableDeclSyntax { 74 | VariableDeclSyntax( 75 | modifiers: [DeclModifierSyntax(name: .keyword(.private))], 76 | bindingSpecifier: .keyword(.let), 77 | bindingsBuilder: { 78 | PatternBindingSyntax( 79 | pattern: IdentifierPatternSyntax(identifier: NS.mocker), 80 | typeAnnotation: TypeAnnotationSyntax(type: IdentifierTypeSyntax(name: NS.Mocker)) 81 | ) 82 | } 83 | ) 84 | } 85 | 86 | private static func initializerDeclaration( 87 | _ kind: BuilderKind, 88 | _ requirements: Requirements 89 | ) -> InitializerDeclSyntax { 90 | InitializerDeclSyntax( 91 | modifiers: requirements.modifiers, 92 | signature: initializerSignature(kind, requirements) 93 | ) { 94 | InfixOperatorExprSyntax( 95 | leftOperand: MemberAccessExprSyntax( 96 | base: DeclReferenceExprSyntax(baseName: .keyword(.self)), 97 | name: NS.mocker 98 | ), 99 | operator: AssignmentExprSyntax(), 100 | rightOperand: DeclReferenceExprSyntax(baseName: NS.mocker) 101 | ) 102 | } 103 | } 104 | 105 | private static func initializerSignature( 106 | _ kind: BuilderKind, 107 | _ requirements: Requirements 108 | ) -> FunctionSignatureSyntax { 109 | FunctionSignatureSyntax( 110 | parameterClause: FunctionParameterClauseSyntax { 111 | FunctionParameterSyntax( 112 | firstName: NS.mocker, 113 | type: IdentifierTypeSyntax(name: NS.Mocker) 114 | ) 115 | } 116 | ) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Factory/Caseable/Caseable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Caseable.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2023. 12. 07.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | /// Describes requirements that are representable by a `Member` enum case. 11 | /// 12 | /// Requirements conforming to `Caseable` provide their case declarations and a 13 | /// utility that helps creating member access expressions for a given case. 14 | protocol Caseable { 15 | /// Returns the member enum represenatation of a syntax. 16 | /// 17 | /// For example a member like: 18 | /// ``` 19 | /// func foo(param: String) -> Int 20 | /// ``` 21 | /// would return the following case declaration: 22 | /// ``` 23 | /// case foo(param: Parameter) 24 | /// ``` 25 | var caseDeclarations: [EnumCaseDeclSyntax] { get throws } 26 | 27 | /// Returns the initializer block that creates the enum case representation. 28 | /// 29 | /// For example a member like: 30 | /// ``` 31 | /// func foo(param: String) -> Int 32 | /// ``` 33 | /// would return the following initializer declaration if wrapParams is true: 34 | /// ``` 35 | /// .m1_foo(param: .value(param)) 36 | /// ``` 37 | /// and: 38 | /// ``` 39 | /// .m1_foo(param: param) 40 | /// ``` 41 | /// if wrapParams is false. 42 | func caseSpecifier(wrapParams: Bool) throws -> ExprSyntax 43 | 44 | /// Returns the initializer block that creates the enum case representation. 45 | /// 46 | /// For example a member like: 47 | /// ``` 48 | /// var prop: Int { get set } 49 | /// ``` 50 | /// would return the following initializer declaration if wrapParams is true: 51 | /// ``` 52 | /// .m1_set_name(newValue: .value(newValue)) 53 | /// ``` 54 | /// and: 55 | /// ``` 56 | /// .m1_set_name(newValue: newValue) 57 | /// ``` 58 | /// if wrapParams is false. 59 | func setterCaseSpecifier(wrapParams: Bool) throws -> ExprSyntax? 60 | } 61 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Factory/Caseable/Function+Caseable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Function+Caseable.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2023. 12. 07.. 6 | // 7 | 8 | import SwiftSyntax 9 | import Foundation 10 | 11 | // MARK: - FunctionRequirement + Caseable 12 | 13 | extension FunctionRequirement: Caseable { 14 | var caseDeclarations: [EnumCaseDeclSyntax] { 15 | get throws { 16 | let enumCase = EnumCaseElementSyntax( 17 | name: caseExpression.declName.baseName, 18 | parameterClause: enumParameterClause 19 | ) 20 | let elements: EnumCaseElementListSyntax = .init(arrayLiteral: enumCase) 21 | return [EnumCaseDeclSyntax(elements: elements)] 22 | } 23 | } 24 | 25 | func caseSpecifier(wrapParams: Bool) throws -> ExprSyntax { 26 | guard let parameters = parameters(wrap: wrapParams) else { 27 | return ExprSyntax(caseExpression) 28 | } 29 | let functionCallExpr = FunctionCallExprSyntax( 30 | calledExpression: caseExpression, 31 | leftParen: .leftParenToken(), 32 | arguments: parameters, 33 | rightParen: .rightParenToken() 34 | ) 35 | return ExprSyntax(functionCallExpr) 36 | } 37 | 38 | func setterCaseSpecifier(wrapParams: Bool) -> ExprSyntax? { nil } 39 | } 40 | 41 | // MARK: - Helpers 42 | 43 | extension FunctionRequirement { 44 | private var caseExpression: MemberAccessExprSyntax { 45 | let indexPrefix = String(index + 1) 46 | let specialEnclosingCharacters: CharacterSet = ["`"] 47 | let caseName = syntax.name.trimmedDescription.trimmingCharacters(in: specialEnclosingCharacters) 48 | return MemberAccessExprSyntax( 49 | name: .identifier("m\(indexPrefix)_\(caseName)") 50 | ) 51 | } 52 | 53 | private func parameters(wrap: Bool) -> LabeledExprListSyntax? { 54 | guard let parameterClause = enumParameterClause else { return nil } 55 | let functionParameters = syntax.signature.parameterClause.parameters 56 | let zippedParameters = zip(parameterClause.parameters, functionParameters) 57 | let enumeratedParameters = zippedParameters.enumerated() 58 | let lastIndex = enumeratedParameters.map(\.offset).last 59 | return LabeledExprListSyntax { 60 | for (index, element) in enumeratedParameters { 61 | let (enumParameter, functionParameter) = element 62 | let hasComma = index != lastIndex && lastIndex != 0 63 | let hasColon = enumParameter.firstName != nil 64 | LabeledExprSyntax( 65 | label: enumParameter.firstName, 66 | colon: hasColon ? .colonToken() : nil, 67 | expression: parameterExpression( 68 | for: functionParameter, 69 | wrapParams: wrap 70 | ), 71 | trailingComma: hasComma ? .commaToken() : nil 72 | ) 73 | } 74 | } 75 | } 76 | 77 | private var enumParameterClause: EnumCaseParameterClauseSyntax? { 78 | guard !syntax.signature.parameterClause.parameters.isEmpty else { return nil } 79 | let enumParameters: EnumCaseParameterListSyntax = .init { 80 | for parameter in syntax.signature.parameterClause.parameters { 81 | let firstName = parameter.firstName.tokenKind == .wildcard 82 | ? nil : parameter.firstName.trimmed 83 | EnumCaseParameterSyntax( 84 | firstName: firstName, 85 | colon: firstName == nil ? nil : .colonToken(), 86 | type: wrappedType(for: parameter) 87 | ) 88 | } 89 | } 90 | return .init(parameters: enumParameters) 91 | } 92 | 93 | private func wrappedType(for parameter: FunctionParameterSyntax) -> IdentifierTypeSyntax { 94 | let type = parameter.resolvedType().description 95 | let isGeneric = syntax.containsGenericType(in: parameter) 96 | let identifier = isGeneric ? NS.GenericValue : type 97 | return IdentifierTypeSyntax(name: NS.Parameter(identifier)) 98 | } 99 | 100 | private func parameterExpression(for functionParameter: FunctionParameterSyntax, wrapParams: Bool) -> ExprSyntax { 101 | if wrapParams { 102 | wrappedParameterExpression(for: functionParameter) 103 | } else { 104 | parameterExpression(for: functionParameter) 105 | } 106 | } 107 | 108 | private func wrappedParameterExpression(for functionParameter: FunctionParameterSyntax) -> ExprSyntax { 109 | let isGeneric = syntax.containsGenericType(in: functionParameter) 110 | let functionParamName = (functionParameter.secondName ?? functionParameter.firstName).declNameOrVarCallName 111 | let functionCallExpr = FunctionCallExprSyntax( 112 | calledExpression: MemberAccessExprSyntax(name: isGeneric ? NS.generic : NS.value), 113 | leftParen: .leftParenToken(), 114 | arguments: LabeledExprListSyntax { 115 | LabeledExprSyntax(expression: DeclReferenceExprSyntax(baseName: functionParamName)) 116 | }, 117 | rightParen: .rightParenToken() 118 | ) 119 | return ExprSyntax(functionCallExpr) 120 | } 121 | 122 | private func parameterExpression(for functionParameter: FunctionParameterSyntax) -> ExprSyntax { 123 | let isGeneric = syntax.containsGenericType(in: functionParameter) 124 | let functionParamName = (functionParameter.secondName ?? functionParameter.firstName).declNameOrVarCallName 125 | if isGeneric { 126 | return ExprSyntax( 127 | FunctionCallExprSyntax( 128 | calledExpression: MemberAccessExprSyntax( 129 | base: DeclReferenceExprSyntax(baseName: functionParamName), 130 | name: NS.eraseToGenericValue 131 | ), 132 | leftParen: .leftParenToken(), 133 | arguments: [], 134 | rightParen: .rightParenToken() 135 | ) 136 | ) 137 | } else { 138 | return ExprSyntax( 139 | DeclReferenceExprSyntax(baseName: functionParamName) 140 | ) 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Factory/Caseable/Variable+Caseable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Variable+Caseable.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 10.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | // MARK: - VariableRequirement + Caseable 11 | 12 | extension VariableRequirement: Caseable { 13 | var caseDeclarations: [EnumCaseDeclSyntax] { 14 | get throws { 15 | [try getterEnumCaseDeclaration, try setterEnumCaseDeclaration].compactMap { $0 } 16 | } 17 | } 18 | 19 | func caseSpecifier(wrapParams: Bool) throws -> ExprSyntax { 20 | ExprSyntax(MemberAccessExprSyntax(name: try getterEnumName)) 21 | } 22 | 23 | func setterCaseSpecifier(wrapParams: Bool) throws -> ExprSyntax? { 24 | guard let setterName = try setterEnumName else { return nil } 25 | let functionCallExpr = FunctionCallExprSyntax( 26 | calledExpression: MemberAccessExprSyntax(name: setterName), 27 | leftParen: .leftParenToken(), 28 | arguments: LabeledExprListSyntax { 29 | LabeledExprSyntax( 30 | label: NS.newValue, 31 | colon: .colonToken(), 32 | expression: wrapParams ? wrappedSetterParameters : setterParameters 33 | ) 34 | }, 35 | rightParen: .rightParenToken() 36 | ) 37 | return ExprSyntax(functionCallExpr) 38 | } 39 | } 40 | 41 | // MARK: - Helpers 42 | 43 | extension VariableRequirement { 44 | private func enumName(prefix: String = "") throws -> TokenSyntax { 45 | .identifier("m\(String(index + 1))_\(prefix)\(try syntax.name)") 46 | } 47 | 48 | private var getterEnumName: TokenSyntax { 49 | get throws { 50 | syntax.isComputed ? try enumName() : try enumName(prefix: NS.get_) 51 | } 52 | } 53 | 54 | private var setterEnumName: TokenSyntax? { 55 | get throws { 56 | syntax.isComputed ? nil : try enumName(prefix: NS.set_) 57 | } 58 | } 59 | 60 | private var setterParameters: ExprSyntax { 61 | ExprSyntax(DeclReferenceExprSyntax(baseName: NS.newValue)) 62 | } 63 | 64 | private var wrappedSetterParameters: ExprSyntax { 65 | let functionCallExpr = FunctionCallExprSyntax( 66 | calledExpression: MemberAccessExprSyntax(name: NS.value), 67 | leftParen: .leftParenToken(), 68 | arguments: LabeledExprListSyntax { 69 | LabeledExprSyntax(expression: DeclReferenceExprSyntax(baseName: NS.newValue)) 70 | }, 71 | rightParen: .rightParenToken() 72 | ) 73 | return ExprSyntax(functionCallExpr) 74 | } 75 | 76 | private var getterEnumCaseDeclaration: EnumCaseDeclSyntax { 77 | get throws { 78 | let getterCase = EnumCaseElementSyntax(name: try getterEnumName) 79 | let elements: EnumCaseElementListSyntax = .init(arrayLiteral: getterCase) 80 | return EnumCaseDeclSyntax(elements: elements) 81 | } 82 | } 83 | 84 | private var setterEnumCaseDeclaration: EnumCaseDeclSyntax? { 85 | get throws { 86 | guard let setterName = try setterEnumName else { return nil } 87 | let setterCase = EnumCaseElementSyntax( 88 | name: setterName, 89 | parameterClause: try setterEnumParameterClause 90 | ) 91 | let elements: EnumCaseElementListSyntax = .init(arrayLiteral: setterCase) 92 | return EnumCaseDeclSyntax(elements: elements) 93 | } 94 | } 95 | 96 | private var wrappedType: IdentifierTypeSyntax { 97 | get throws { 98 | let baseType = try syntax.resolvedType.trimmedDescription 99 | return IdentifierTypeSyntax(name: NS.Parameter(baseType)) 100 | } 101 | } 102 | 103 | private var setterEnumParameterClause: EnumCaseParameterClauseSyntax? { 104 | get throws { 105 | guard !syntax.isComputed else { return nil } 106 | let parameter = EnumCaseParameterSyntax( 107 | firstName: NS.newValue, 108 | colon: .colonToken(), 109 | type: try wrappedType 110 | ) 111 | return EnumCaseParameterClauseSyntax( 112 | parameters: EnumCaseParameterListSyntax([parameter]) 113 | ) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Factory/ConformanceFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConformanceFactory.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 23.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | /// Factory to generate mock conformances of requirements. 11 | /// 12 | /// Returns a member block item list that includes a mock implementation for every requirement. 13 | enum ConformanceFactory: Factory { 14 | static func build(from requirements: Requirements) throws -> MemberBlockItemListSyntax { 15 | try MemberBlockItemListSyntax { 16 | try inits(requirements) 17 | try functions(requirements) 18 | try variables(requirements) 19 | } 20 | } 21 | } 22 | 23 | // MARK: - Helpers 24 | 25 | extension ConformanceFactory { 26 | private static func variables(_ requirements: Requirements) throws -> MemberBlockItemListSyntax { 27 | try MemberBlockItemListSyntax { 28 | for variable in requirements.variables { 29 | MemberBlockItemSyntax( 30 | decl: try variable.implement(with: requirements.modifiers) 31 | ) 32 | } 33 | } 34 | } 35 | 36 | private static func functions(_ requirements: Requirements) throws -> MemberBlockItemListSyntax { 37 | try MemberBlockItemListSyntax { 38 | for function in requirements.functions { 39 | MemberBlockItemSyntax( 40 | decl: try function.implement(with: requirements.modifiers) 41 | ) 42 | } 43 | } 44 | } 45 | 46 | private static func inits(_ requirements: Requirements) throws -> MemberBlockItemListSyntax { 47 | try MemberBlockItemListSyntax { 48 | for initializer in requirements.initializers { 49 | MemberBlockItemSyntax( 50 | decl: try initializer.implement(with: requirements.modifiers) 51 | ) 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Factory/Factory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Factory.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 28.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | /// Defines a generic factory protocol for generating syntax of the mock implementation. 11 | /// 12 | /// This protocol serves as a blueprint for factory types that transform the protocol requirements 13 | /// into a mock implementation. 14 | protocol Factory { 15 | /// The type of syntax produced by the factory. 16 | associatedtype Result: SyntaxProtocol 17 | 18 | /// Generates a syntax using the given requirements. 19 | /// 20 | /// - Parameter requirements: Groupped protocol requirements to generate syntax for. 21 | /// - Throws: Throws an error if the construction fails, which could be due to invalid 22 | /// requirements or other issues that prevent the successful generation of the result. 23 | /// - Returns: An instance of the associated `Result` type, representing the generated 24 | /// syntax structure. 25 | static func build(from requirements: Requirements) throws -> Result 26 | } 27 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Factory/MockFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockFactory.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 28.. 6 | // 7 | 8 | #if canImport(SwiftSyntax600) || swift(<6) 9 | import SwiftSyntax 10 | #else 11 | @preconcurrency import SwiftSyntax 12 | #endif 13 | 14 | /// Factory to generate the mock service declaration. 15 | /// 16 | /// Generates a class declaration that defines the mock implementation of the protocol. 17 | enum MockFactory: Factory { 18 | static func build(from requirements: Requirements) throws -> DeclSyntax { 19 | let classDecl = ClassDeclSyntax( 20 | leadingTrivia: leadingTrivia(requirements), 21 | attributes: try attributes(requirements), 22 | modifiers: modifiers(requirements), 23 | classKeyword: classKeyword(requirements), 24 | name: .identifier(requirements.syntax.mockName), 25 | genericParameterClause: genericParameterClause(requirements), 26 | inheritanceClause: inheritanceClause(requirements), 27 | genericWhereClause: genericWhereClause(requirements), 28 | memberBlock: try MemberBlockSyntax { 29 | try MemberFactory.build(from: requirements) 30 | try ConformanceFactory.build(from: requirements) 31 | try EnumFactory.build(from: requirements) 32 | try BuilderFactory.build(from: requirements) 33 | } 34 | ) 35 | return DeclSyntax(classDecl) 36 | } 37 | } 38 | 39 | // MARK: - Helpers 40 | 41 | extension MockFactory { 42 | private static func leadingTrivia(_ requirements: Requirements) -> Trivia { 43 | requirements.syntax.leadingTrivia 44 | } 45 | 46 | private static let inheritedTypeMappings: [String: TokenSyntax] = [ 47 | NS.NSObjectProtocol: NS.NSObject 48 | ] 49 | 50 | private static func attributes(_ requirements: Requirements) throws -> AttributeListSyntax { 51 | guard requirements.containsGenericExistentials else { return [] } 52 | return try AttributeListSyntax { 53 | // Runtime support for parametrized protocol types is only available from: 54 | try Availability.from(iOS: "16.0", macOS: "13.0", tvOS: "16.0", watchOS: "9.0") 55 | } 56 | .with(\.trailingTrivia, .newline) 57 | } 58 | 59 | private static func modifiers(_ requirements: Requirements) -> DeclModifierListSyntax { 60 | var modifiers = requirements.syntax.modifiers.trimmed 61 | if !requirements.isActor { 62 | modifiers.append(DeclModifierSyntax(name: .keyword(.final))) 63 | } 64 | return modifiers 65 | } 66 | 67 | private static func classKeyword(_ requirements: Requirements) -> TokenSyntax { 68 | requirements.isActor ? .keyword(.actor) : .keyword(.class) 69 | } 70 | 71 | private static func inheritanceClause(_ requirements: Requirements) -> InheritanceClauseSyntax { 72 | InheritanceClauseSyntax { 73 | inheritedTypeMappings(requirements) 74 | InheritedTypeSyntax(type: IdentifierTypeSyntax( 75 | name: requirements.syntax.name.trimmed 76 | )) 77 | InheritedTypeSyntax( 78 | type: MemberTypeSyntax( 79 | baseType: IdentifierTypeSyntax(name: NS.Mockable), 80 | name: NS.MockableService 81 | ) 82 | ) 83 | } 84 | } 85 | 86 | private static func inheritedTypeMappings(_ requirements: Requirements) -> InheritedTypeListSyntax { 87 | guard let inheritanceClause = requirements.syntax.inheritanceClause else { return [] } 88 | return InheritedTypeListSyntax { 89 | for inheritedType in inheritanceClause.inheritedTypes { 90 | if let type = inheritedType.type.as(IdentifierTypeSyntax.self), 91 | let mapping = inheritedTypeMappings[type.name.trimmedDescription] { 92 | InheritedTypeSyntax(type: IdentifierTypeSyntax(name: mapping)) 93 | } 94 | } 95 | } 96 | } 97 | 98 | private static func getAssociatedTypes(_ requirements: Requirements) -> [AssociatedTypeDeclSyntax] { 99 | requirements.syntax.memberBlock.members.compactMap { 100 | $0.decl.as(AssociatedTypeDeclSyntax.self) 101 | } 102 | } 103 | 104 | private static func genericParameterClause(_ requirements: Requirements) -> GenericParameterClauseSyntax? { 105 | let associatedTypes = getAssociatedTypes(requirements) 106 | guard !associatedTypes.isEmpty else { return nil } 107 | return .init { 108 | GenericParameterListSyntax { 109 | for name in associatedTypes.map(\.name.trimmed) { 110 | GenericParameterSyntax(name: name) 111 | } 112 | } 113 | } 114 | } 115 | 116 | private static func genericWhereClause(_ requirements: Requirements) -> GenericWhereClauseSyntax? { 117 | let associatedTypes = getAssociatedTypes(requirements) 118 | guard !associatedTypes.isEmpty else { return nil } 119 | 120 | let inheritances = associatedTypes.filter { $0.inheritanceClause != nil } 121 | let whereClauses = associatedTypes.filter { $0.genericWhereClause != nil } 122 | 123 | guard !inheritances.isEmpty || !whereClauses.isEmpty else { return nil } 124 | 125 | let requirementList = GenericRequirementListSyntax { 126 | if let genericWhereClause = requirements.syntax.genericWhereClause { 127 | genericWhereClause.requirements 128 | } 129 | 130 | for type in whereClauses { 131 | if let genericWhereClause = type.genericWhereClause { 132 | genericWhereClause.requirements 133 | } 134 | } 135 | 136 | for type in inheritances { 137 | if let inheritanceClause = type.inheritanceClause { 138 | for inheritedType in inheritanceClause.inheritedTypes { 139 | let requirement = ConformanceRequirementSyntax( 140 | leftType: IdentifierTypeSyntax(name: type.name), 141 | rightType: inheritedType.type 142 | ) 143 | GenericRequirementSyntax( 144 | requirement: .conformanceRequirement(requirement) 145 | ) 146 | } 147 | } 148 | } 149 | } 150 | 151 | return .init(requirements: requirementList) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Factory/Mockable/Function+Mockable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Function+Mockable.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 23.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | // MARK: - FunctionRequirement + Mockable 11 | 12 | extension FunctionRequirement: Mockable { 13 | func implement(with modifiers: DeclModifierListSyntax) throws -> DeclSyntax { 14 | let decl = FunctionDeclSyntax( 15 | attributes: syntax.attributes.trimmed.with(\.trailingTrivia, .newline), 16 | modifiers: declarationModifiers(extending: modifiers), 17 | funcKeyword: syntax.funcKeyword.trimmed, 18 | name: syntax.name.trimmed, 19 | genericParameterClause: syntax.genericParameterClause?.trimmed, 20 | signature: syntax.signature.trimmed, 21 | genericWhereClause: syntax.genericWhereClause?.trimmed, 22 | body: .init(statements: try body) 23 | ) 24 | for parameter in decl.signature.parameterClause.parameters { 25 | guard parameter.type.as(FunctionTypeSyntax.self) == nil else { 26 | throw MockableMacroError.nonEscapingFunctionParameter 27 | } 28 | } 29 | return DeclSyntax(decl) 30 | } 31 | } 32 | 33 | // MARK: - Helpers 34 | 35 | extension FunctionRequirement { 36 | private func declarationModifiers(extending modifiers: DeclModifierListSyntax) -> DeclModifierListSyntax { 37 | let filtered = syntax.modifiers.filtered(keywords: [.nonisolated]) 38 | return modifiers.trimmed.appending(filtered.trimmed) 39 | } 40 | 41 | private var body: CodeBlockItemListSyntax { 42 | get throws { 43 | try CodeBlockItemListSyntax { 44 | try memberDeclaration 45 | if syntax.isVoid { 46 | mockerCall 47 | } else { 48 | returnStatement 49 | } 50 | } 51 | } 52 | } 53 | 54 | private var memberDeclaration: DeclSyntax { 55 | get throws { 56 | let variableDecl = try VariableDeclSyntax(bindingSpecifier: .keyword(.let)) { 57 | try PatternBindingListSyntax { 58 | PatternBindingSyntax( 59 | pattern: IdentifierPatternSyntax(identifier: NS.member), 60 | typeAnnotation: TypeAnnotationSyntax( 61 | type: IdentifierTypeSyntax(name: NS.Member) 62 | ), 63 | initializer: InitializerClauseSyntax( 64 | value: try caseSpecifier(wrapParams: true) 65 | ) 66 | ) 67 | } 68 | } 69 | return DeclSyntax(variableDecl) 70 | } 71 | } 72 | 73 | private var returnStatement: StmtSyntax { 74 | StmtSyntax(ReturnStmtSyntax(expression: mockerCall)) 75 | } 76 | 77 | private var mockerCall: ExprSyntax { 78 | let call = FunctionCallExprSyntax( 79 | calledExpression: MemberAccessExprSyntax( 80 | base: DeclReferenceExprSyntax(baseName: NS.mocker), 81 | declName: DeclReferenceExprSyntax( 82 | baseName: syntax.isThrowing ? NS.mockThrowing : NS.mock 83 | ) 84 | ), 85 | leftParen: .leftParenToken(), 86 | arguments: LabeledExprListSyntax { 87 | LabeledExprSyntax( 88 | expression: DeclReferenceExprSyntax(baseName: NS.member) 89 | ) 90 | if let errorTypeExpression { 91 | errorTypeExpression 92 | } 93 | }, 94 | rightParen: .rightParenToken(), 95 | trailingClosure: mockerClosure 96 | ) 97 | return if syntax.isThrowing { 98 | ExprSyntax(TryExprSyntax(expression: call)) 99 | } else { 100 | ExprSyntax(call) 101 | } 102 | } 103 | 104 | private var errorTypeExpression: LabeledExprSyntax? { 105 | #if canImport(SwiftSyntax600) 106 | guard let type = syntax.errorType else { return nil } 107 | return LabeledExprSyntax( 108 | label: NS.error, 109 | colon: .colonToken(), 110 | expression: MemberAccessExprSyntax( 111 | base: DeclReferenceExprSyntax(baseName: .identifier(type.trimmedDescription)), 112 | declName: DeclReferenceExprSyntax(baseName: .keyword(.`self`)) 113 | ) 114 | ) 115 | #else 116 | return nil 117 | #endif 118 | } 119 | 120 | private var mockerClosure: ClosureExprSyntax { 121 | ClosureExprSyntax( 122 | signature: mockerClosureSignature, 123 | statements: CodeBlockItemListSyntax { 124 | producerDeclaration 125 | producerCall 126 | } 127 | ) 128 | } 129 | 130 | private var mockerClosureSignature: ClosureSignatureSyntax { 131 | let paramList = ClosureShorthandParameterListSyntax { 132 | ClosureShorthandParameterSyntax(name: NS.producer) 133 | } 134 | return ClosureSignatureSyntax(parameterClause: .simpleInput(paramList)) 135 | } 136 | 137 | private var producerDeclaration: VariableDeclSyntax { 138 | VariableDeclSyntax( 139 | bindingSpecifier: .keyword(.let), 140 | bindings: PatternBindingListSyntax { 141 | PatternBindingSyntax( 142 | pattern: IdentifierPatternSyntax(identifier: NS.producer), 143 | initializer: InitializerClauseSyntax(value: producerCast) 144 | ) 145 | } 146 | ) 147 | } 148 | 149 | private var producerCast: TryExprSyntax { 150 | TryExprSyntax( 151 | expression: AsExprSyntax( 152 | expression: FunctionCallExprSyntax( 153 | calledExpression: DeclReferenceExprSyntax(baseName: NS.cast), 154 | leftParen: .leftParenToken(), 155 | arguments: LabeledExprListSyntax { 156 | LabeledExprSyntax( 157 | expression: DeclReferenceExprSyntax(baseName: NS.producer) 158 | ) 159 | }, 160 | rightParen: .rightParenToken() 161 | ), 162 | type: syntax.closureType 163 | ) 164 | ) 165 | } 166 | 167 | private var producerCall: ReturnStmtSyntax { 168 | let producerCallExpr = FunctionCallExprSyntax( 169 | calledExpression: DeclReferenceExprSyntax(baseName: NS.producer), 170 | leftParen: .leftParenToken(), 171 | arguments: LabeledExprListSyntax { 172 | for parameter in syntax.signature.parameterClause.parameters { 173 | let parameterReference = DeclReferenceExprSyntax( 174 | baseName: (parameter.secondName?.trimmed ?? parameter.firstName.trimmed).declNameOrVarCallName 175 | ) 176 | if parameter.isInout { 177 | LabeledExprSyntax(expression: InOutExprSyntax(expression: parameterReference)) 178 | } else { 179 | LabeledExprSyntax(expression: parameterReference) 180 | } 181 | } 182 | }, 183 | rightParen: .rightParenToken() 184 | ) 185 | let producerCall = ExprSyntax(producerCallExpr) 186 | let tryProducerCall = ExprSyntax(TryExprSyntax(expression: producerCall)) 187 | 188 | return ReturnStmtSyntax(expression: syntax.isThrowing ? tryProducerCall : producerCall) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Factory/Mockable/Initializer+Mockable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Initializer+Mockable.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 23.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | // MARK: - InitializerRequirement + Mockable 11 | 12 | extension InitializerRequirement: Mockable { 13 | func implement(with modifiers: DeclModifierListSyntax) throws -> DeclSyntax { 14 | let initDecl = InitializerDeclSyntax( 15 | attributes: syntax.attributes.trimmed.with(\.trailingTrivia, .newline), 16 | modifiers: declarationModifiers(extending: modifiers), 17 | initKeyword: syntax.initKeyword.trimmed, 18 | optionalMark: syntax.optionalMark?.trimmed, 19 | genericParameterClause: syntax.genericParameterClause?.trimmed, 20 | signature: syntax.signature.trimmed, 21 | genericWhereClause: syntax.genericWhereClause?.trimmed, 22 | body: .init(statements: []) 23 | ) 24 | return DeclSyntax(initDecl) 25 | } 26 | } 27 | 28 | // MARK: - Helpers 29 | 30 | extension InitializerRequirement { 31 | private func declarationModifiers(extending modifiers: DeclModifierListSyntax) -> DeclModifierListSyntax { 32 | let filtered = syntax.modifiers.filtered(keywords: [.nonisolated]) 33 | return modifiers.trimmed.appending(filtered.trimmed) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Factory/Mockable/Mockable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mockable.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 23.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | /// Defines a protocol for creating mock implementations. 11 | /// 12 | /// Requirements conforming to `Mockable` can generate mock versions of themselves. 13 | protocol Mockable { 14 | /// Creates a mock declaration. 15 | /// 16 | /// - Parameter modifiers: Modifiers to apply to the mock declaration. 17 | /// - Throws: If the mock cannot be generated. 18 | /// - Returns: The mock declaration of the requirements. 19 | func implement(with modifiers: DeclModifierListSyntax) throws -> DeclSyntax 20 | } 21 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Factory/Mockable/Variable+Mockable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Variable+Mockable.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 23.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | // MARK: - VariableRequirement + Mockable 11 | 12 | extension VariableRequirement: Mockable { 13 | func implement(with modifiers: DeclModifierListSyntax) throws -> DeclSyntax { 14 | let variableDecl = VariableDeclSyntax( 15 | attributes: syntax.attributes.trimmed.with(\.trailingTrivia, .newline), 16 | modifiers: declarationModifiers(extending: modifiers), 17 | bindingSpecifier: .keyword(.var), 18 | bindings: try PatternBindingListSyntax { 19 | try syntax.binding.with(\.accessorBlock, accessorBlock) 20 | } 21 | ) 22 | return DeclSyntax(variableDecl) 23 | } 24 | } 25 | 26 | // MARK: - Helpers 27 | 28 | extension VariableRequirement { 29 | private func declarationModifiers(extending modifiers: DeclModifierListSyntax) -> DeclModifierListSyntax { 30 | let filtered = syntax.modifiers.filtered(keywords: [.nonisolated]) 31 | return modifiers.trimmed.appending(filtered.trimmed) 32 | } 33 | 34 | private var accessorBlock: AccessorBlockSyntax { 35 | get throws { 36 | AccessorBlockSyntax(accessors: .accessors(try accessorDeclList)) 37 | } 38 | } 39 | 40 | private var accessorDeclList: AccessorDeclListSyntax { 41 | get throws { 42 | try AccessorDeclListSyntax { 43 | try getterDecl 44 | if let setterDecl = try setterDecl { 45 | setterDecl 46 | } 47 | } 48 | } 49 | } 50 | 51 | private var getterDecl: AccessorDeclSyntax { 52 | get throws { 53 | let caseSpecifier = try caseSpecifier(wrapParams: true) 54 | let mockerCall = try mockerCall 55 | let body = CodeBlockSyntax( 56 | statements: CodeBlockItemListSyntax { 57 | memberDeclaration(caseSpecifier) 58 | ReturnStmtSyntax(expression: mockerCall) 59 | } 60 | ) 61 | return try syntax.getAccessor.with(\.body, body) 62 | } 63 | } 64 | 65 | private var setterDecl: AccessorDeclSyntax? { 66 | get throws { 67 | guard let caseSpecifier = try setterCaseSpecifier(wrapParams: true), 68 | let setAccessor = syntax.setAccessor else { return nil } 69 | let body = CodeBlockSyntax( 70 | statements: CodeBlockItemListSyntax { 71 | memberDeclaration(caseSpecifier) 72 | mockerCall(memberName: NS.addInvocation) 73 | mockerCall(memberName: NS.performActions) 74 | } 75 | ) 76 | return setAccessor.with(\.body, body) 77 | } 78 | } 79 | 80 | private var mockerCall: ExprSyntax { 81 | get throws { 82 | let call = FunctionCallExprSyntax( 83 | calledExpression: MemberAccessExprSyntax( 84 | base: DeclReferenceExprSyntax(baseName: NS.mocker), 85 | declName: DeclReferenceExprSyntax( 86 | baseName: try syntax.isThrowing ? NS.mockThrowing : NS.mock 87 | ) 88 | ), 89 | leftParen: .leftParenToken(), 90 | arguments: try LabeledExprListSyntax { 91 | LabeledExprSyntax( 92 | expression: DeclReferenceExprSyntax(baseName: NS.member) 93 | ) 94 | if let errorTypeExpression = try errorTypeExpression { 95 | errorTypeExpression 96 | } 97 | }, 98 | rightParen: .rightParenToken(), 99 | trailingClosure: try mockerClosure 100 | ) 101 | return if try syntax.isThrowing { 102 | ExprSyntax(TryExprSyntax(expression: call)) 103 | } else { 104 | ExprSyntax(call) 105 | } 106 | } 107 | } 108 | 109 | private var errorTypeExpression: LabeledExprSyntax? { 110 | get throws { 111 | #if canImport(SwiftSyntax600) 112 | guard let type = try syntax.errorType else { return nil } 113 | return LabeledExprSyntax( 114 | label: NS.error, 115 | colon: .colonToken(), 116 | expression: MemberAccessExprSyntax( 117 | base: DeclReferenceExprSyntax(baseName: .identifier(type.trimmedDescription)), 118 | declName: DeclReferenceExprSyntax(baseName: .keyword(.`self`)) 119 | ) 120 | ) 121 | #else 122 | return nil 123 | #endif 124 | } 125 | } 126 | 127 | private func memberDeclaration(_ caseSpecifier: ExprSyntax) -> VariableDeclSyntax { 128 | VariableDeclSyntax(bindingSpecifier: .keyword(.let)) { 129 | PatternBindingListSyntax { 130 | PatternBindingSyntax( 131 | pattern: IdentifierPatternSyntax(identifier: NS.member), 132 | typeAnnotation: TypeAnnotationSyntax( 133 | type: IdentifierTypeSyntax(name: NS.Member) 134 | ), 135 | initializer: InitializerClauseSyntax(value: caseSpecifier) 136 | ) 137 | } 138 | } 139 | } 140 | 141 | private var mockerClosure: ClosureExprSyntax { 142 | get throws { 143 | ClosureExprSyntax( 144 | signature: mockerClosureSignature, 145 | statements: try CodeBlockItemListSyntax { 146 | try producerDeclaration 147 | try producerCall 148 | } 149 | ) 150 | } 151 | } 152 | 153 | private var mockerClosureSignature: ClosureSignatureSyntax { 154 | let paramList = ClosureShorthandParameterListSyntax { 155 | ClosureShorthandParameterSyntax(name: NS.producer) 156 | } 157 | return ClosureSignatureSyntax(parameterClause: .simpleInput(paramList)) 158 | } 159 | 160 | private var producerDeclaration: VariableDeclSyntax { 161 | get throws { 162 | VariableDeclSyntax( 163 | bindingSpecifier: .keyword(.let), 164 | bindings: try PatternBindingListSyntax { 165 | PatternBindingSyntax( 166 | pattern: IdentifierPatternSyntax(identifier: NS.producer), 167 | initializer: InitializerClauseSyntax(value: try producerCast) 168 | ) 169 | } 170 | ) 171 | } 172 | } 173 | 174 | private var producerCast: TryExprSyntax { 175 | get throws { 176 | TryExprSyntax( 177 | expression: AsExprSyntax( 178 | expression: FunctionCallExprSyntax( 179 | calledExpression: DeclReferenceExprSyntax(baseName: NS.cast), 180 | leftParen: .leftParenToken(), 181 | arguments: LabeledExprListSyntax { 182 | LabeledExprSyntax( 183 | expression: DeclReferenceExprSyntax(baseName: NS.producer) 184 | ) 185 | }, 186 | rightParen: .rightParenToken() 187 | ), 188 | type: try syntax.closureType 189 | ) 190 | ) 191 | } 192 | } 193 | 194 | private var producerCall: ReturnStmtSyntax { 195 | get throws { 196 | let producerCallExpr = FunctionCallExprSyntax( 197 | calledExpression: DeclReferenceExprSyntax(baseName: NS.producer), 198 | leftParen: .leftParenToken(), 199 | arguments: [], 200 | rightParen: .rightParenToken() 201 | ) 202 | 203 | let producerCall = ExprSyntax(producerCallExpr) 204 | let tryProducerCall = ExprSyntax(TryExprSyntax(expression: producerCall)) 205 | 206 | return ReturnStmtSyntax(expression: try syntax.isThrowing ? tryProducerCall : producerCall) 207 | } 208 | } 209 | 210 | private func mockerCall(memberName: TokenSyntax) -> FunctionCallExprSyntax { 211 | FunctionCallExprSyntax( 212 | calledExpression: MemberAccessExprSyntax( 213 | base: DeclReferenceExprSyntax(baseName: NS.mocker), 214 | name: memberName 215 | ), 216 | leftParen: .leftParenToken(), 217 | arguments: LabeledExprListSyntax { 218 | LabeledExprSyntax( 219 | label: NS._for, 220 | colon: .colonToken(), 221 | expression: DeclReferenceExprSyntax(baseName: NS.member) 222 | ) 223 | }, 224 | rightParen: .rightParenToken() 225 | ) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /Sources/MockableMacro/MockableMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockableMacro.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 14.. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftSyntaxMacros 10 | import SwiftDiagnostics 11 | 12 | public enum MockableMacro: PeerMacro { 13 | public static func expansion( 14 | of node: AttributeSyntax, 15 | providingPeersOf declaration: some DeclSyntaxProtocol, 16 | in context: some MacroExpansionContext 17 | ) throws -> [DeclSyntax] { 18 | guard let protocolDecl = declaration.as(ProtocolDeclSyntax.self) else { 19 | throw MockableMacroError.notAProtocol 20 | } 21 | 22 | #if swift(>=6) && !canImport(SwiftSyntax600) 23 | context.diagnose(Diagnostic(node: node, message: MockableMacroWarning.versionMismatch)) 24 | #endif 25 | 26 | let requirements = try Requirements(protocolDecl) 27 | let declaration = try MockFactory.build(from: requirements) 28 | let codeblock = CodeBlockItemListSyntax { 29 | CodeBlockItemSyntax(item: .decl(declaration)) 30 | } 31 | 32 | let ifClause = IfConfigClauseListSyntax { 33 | IfConfigClauseSyntax( 34 | poundKeyword: .poundIfToken(), 35 | condition: DeclReferenceExprSyntax(baseName: NS.MOCKING), 36 | elements: .statements(codeblock) 37 | ) 38 | } 39 | 40 | return [DeclSyntax(IfConfigDeclSyntax(clauses: ifClause))] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/MockableMacro/MockableMacroError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockableMacroError.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 24.. 6 | // 7 | 8 | public enum MockableMacroError: Error, CustomStringConvertible { 9 | case notAProtocol 10 | case invalidVariableRequirement 11 | case invalidDerivedEnumCase 12 | case nonEscapingFunctionParameter 13 | case subscriptsNotSupported 14 | case operatorsNotSupported 15 | case staticMembersNotSupported 16 | 17 | public var description: String { 18 | switch self { 19 | case .notAProtocol: 20 | return "@Mockable can only be applied to protocols." 21 | case .invalidVariableRequirement: 22 | return "Invalid variable requirement. Missing type annotation or accessor block." 23 | case .invalidDerivedEnumCase: 24 | return "Unexpected error during generating an enum representation of this protocol." 25 | case .nonEscapingFunctionParameter: 26 | return """ 27 | Non-escaping function parameters are not supported by @Mockable. \ 28 | Add @escaping to all function parameters to resolve this issue. 29 | """ 30 | case .subscriptsNotSupported: 31 | return "Subscript requirements are not supported by @Mockable." 32 | case .operatorsNotSupported: 33 | return "Operator requirements are not supported by @Mockable." 34 | case .staticMembersNotSupported: 35 | return "Static member requirements are not supported by @Mockable." 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/MockableMacro/MockableMacroWarning.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockableMacroWarning.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 2024. 12. 01.. 6 | // 7 | 8 | import SwiftSyntax 9 | import SwiftDiagnostics 10 | 11 | public enum MockableMacroWarning { 12 | case versionMismatch 13 | } 14 | 15 | // MARK: - DiagnosticMessage 16 | 17 | extension MockableMacroWarning: DiagnosticMessage { 18 | public var message: String { 19 | switch self { 20 | case .versionMismatch: """ 21 | Your SwiftSyntax version is pinned to \(swiftSyntaxVersion).x.x by some of your dependencies. \ 22 | Using a lower SwiftSyntax version than your Swift version may lead to issues when using Mockable. 23 | """ 24 | } 25 | } 26 | public var diagnosticID: MessageID { 27 | switch self { 28 | case .versionMismatch: MessageID(domain: "Mockable", id: "MockableMacroWarning.versionMismatch") 29 | } 30 | } 31 | public var severity: DiagnosticSeverity { .warning } 32 | } 33 | 34 | // MARK: - Helpers 35 | 36 | extension MockableMacroWarning { 37 | private var swiftSyntaxVersion: String { 38 | #if canImport(SwiftSyntax600) 39 | ">600" 40 | #elseif canImport(SwiftSyntax510) 41 | "510" 42 | #elseif canImport(SwiftSyntax509) 43 | "509" 44 | #else 45 | "" 46 | #endif 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Plugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockableMacroPlugin.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 14.. 6 | // 7 | 8 | import SwiftCompilerPlugin 9 | import SwiftSyntaxMacros 10 | 11 | @main 12 | struct Plugin: CompilerPlugin { 13 | let providingMacros: [any Macro.Type] = [ 14 | MockableMacro.self 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Requirements/FunctionRequirement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FunctionRequirement.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2023. 12. 07.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | struct FunctionRequirement { 11 | let index: Int 12 | let syntax: FunctionDeclSyntax 13 | } 14 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Requirements/InitializerRequirement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InitializerRequirement.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 14.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | struct InitializerRequirement { 11 | let index: Int 12 | let syntax: InitializerDeclSyntax 13 | } 14 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Requirements/Requirements.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Requirements.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 12.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | struct Requirements { 11 | 12 | // MARK: Properties 13 | 14 | let syntax: ProtocolDeclSyntax 15 | let modifiers: DeclModifierListSyntax 16 | let isActor: Bool 17 | let containsGenericExistentials: Bool 18 | var functions = [FunctionRequirement]() 19 | var variables = [VariableRequirement]() 20 | var initializers = [InitializerRequirement]() 21 | 22 | // MARK: Init 23 | 24 | init(_ syntax: ProtocolDeclSyntax) throws { 25 | self.syntax = syntax 26 | let members = syntax.memberBlock.members 27 | 28 | guard members.compactMap({ $0.decl.as(SubscriptDeclSyntax.self) }).isEmpty else { 29 | throw MockableMacroError.subscriptsNotSupported 30 | } 31 | 32 | self.modifiers = Self.initModifiers(syntax) 33 | self.isActor = Self.initIsActor(syntax) 34 | self.initializers = Self.initInitializers(members) 35 | self.variables = try Self.initVariables(members) 36 | self.functions = try Self.initFunctions(members, startIndex: variables.count) 37 | self.containsGenericExistentials = try Self.initContainsGenericExistentials(variables, functions) 38 | } 39 | } 40 | 41 | // MARK: - Helpers 42 | 43 | extension Requirements { 44 | private static func isStatic(_ modifier: DeclModifierSyntax) -> Bool { 45 | modifier.name.tokenKind == .keyword(.static) 46 | } 47 | 48 | private static func initModifiers(_ syntax: ProtocolDeclSyntax) -> DeclModifierListSyntax { 49 | syntax.modifiers.trimmed.filter { modifier in 50 | guard case .keyword(let keyword) = modifier.name.tokenKind else { 51 | return true 52 | } 53 | return keyword != .private 54 | } 55 | } 56 | 57 | private static func initIsActor(_ syntax: ProtocolDeclSyntax) -> Bool { 58 | guard let inheritanceClause = syntax.inheritanceClause, 59 | !inheritanceClause.inheritedTypes.isEmpty else { 60 | return false 61 | } 62 | 63 | for inheritedType in inheritanceClause.inheritedTypes { 64 | if let type = inheritedType.type.as(IdentifierTypeSyntax.self), 65 | type.name.trimmed.tokenKind == NS.Actor.tokenKind { 66 | return true 67 | } 68 | } 69 | 70 | return false 71 | } 72 | 73 | private static func initVariables(_ members: MemberBlockItemListSyntax) throws -> [VariableRequirement] { 74 | try members 75 | .compactMap { $0.decl.as(VariableDeclSyntax.self) } 76 | .filter { 77 | guard !$0.modifiers.contains(where: isStatic) else { 78 | throw MockableMacroError.staticMembersNotSupported 79 | } 80 | return true 81 | } 82 | .enumerated() 83 | .map(VariableRequirement.init) 84 | } 85 | 86 | private static func initFunctions(_ members: MemberBlockItemListSyntax, 87 | startIndex: Int) throws -> [FunctionRequirement] { 88 | try members 89 | .compactMap { $0.decl.as(FunctionDeclSyntax.self) } 90 | .filter { 91 | guard !$0.modifiers.contains(where: isStatic) else { 92 | throw MockableMacroError.staticMembersNotSupported 93 | } 94 | guard case .identifier = $0.name.tokenKind else { 95 | throw MockableMacroError.operatorsNotSupported 96 | } 97 | return true 98 | } 99 | .enumerated() 100 | .map { index, element in 101 | FunctionRequirement(index: startIndex + index, syntax: element) 102 | } 103 | } 104 | 105 | private static func initInitializers(_ members: MemberBlockItemListSyntax) -> [InitializerRequirement] { 106 | members 107 | .compactMap { $0.decl.as(InitializerDeclSyntax.self) } 108 | .enumerated() 109 | .map { InitializerRequirement(index: $0, syntax: $1) } 110 | } 111 | 112 | private static func initContainsGenericExistentials( 113 | _ variables: [VariableRequirement], 114 | _ functions: [FunctionRequirement] 115 | ) throws -> Bool { 116 | let variables = try variables.filter { 117 | let type = try $0.syntax.type 118 | return hasParametrizedProtocolRequirement(type) 119 | } 120 | 121 | let functions = functions.filter { 122 | guard let returnClause = $0.syntax.signature.returnClause else { return false } 123 | let type = returnClause.type 124 | return hasParametrizedProtocolRequirement(type) 125 | } 126 | 127 | return !variables.isEmpty || !functions.isEmpty 128 | } 129 | 130 | private static func hasParametrizedProtocolRequirement(_ type: TypeSyntax) -> Bool { 131 | if let type = type.as(SomeOrAnyTypeSyntax.self), 132 | type.someOrAnySpecifier.tokenKind == .keyword(.any), 133 | let type = type.constraint.as(IdentifierTypeSyntax.self), 134 | let argumentClause = type.genericArgumentClause, 135 | !argumentClause.arguments.isEmpty { 136 | return true 137 | } else if let type = type.as(IdentifierTypeSyntax.self), 138 | let argumentClause = type.genericArgumentClause { 139 | return argumentClause.arguments.contains { 140 | return hasParametrizedProtocolRequirement($0.argument) 141 | } 142 | } else { 143 | return false 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Requirements/VariableRequirement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VariableRequirement.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 10.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | struct VariableRequirement { 11 | let index: Int 12 | let syntax: VariableDeclSyntax 13 | } 14 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Utils/Availability.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Availability.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 30/07/2024. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | enum AvailabilityVersionParseError: Error { 11 | case invalidVersionString 12 | } 13 | 14 | enum Availability { 15 | static func from(iOS: String, macOS: String, tvOS: String, watchOS: String) throws -> AttributeSyntax { 16 | let arguments = try AvailabilityArgumentListSyntax { 17 | try availability(platform: NS.iOS, version: iOS) 18 | try availability(platform: NS.macOS, version: macOS) 19 | try availability(platform: NS.tvOS, version: tvOS) 20 | try availability(platform: NS.watchOS, version: watchOS) 21 | AvailabilityArgumentSyntax(argument: .token(.binaryOperator("*"))) 22 | } 23 | 24 | return AttributeSyntax( 25 | attributeName: IdentifierTypeSyntax(name: NS.available), 26 | leftParen: .leftParenToken(), 27 | arguments: .availability(arguments), 28 | rightParen: .rightParenToken() 29 | ) 30 | } 31 | 32 | private static func availability(platform: TokenSyntax, version: String) throws -> AvailabilityArgumentSyntax { 33 | let version = version.split(separator: ".").map(String.init) 34 | 35 | guard let major = version.first else { 36 | throw AvailabilityVersionParseError.invalidVersionString 37 | } 38 | let components = version.dropFirst().compactMap { 39 | return VersionComponentSyntax(number: .integerLiteral($0)) 40 | } 41 | 42 | let versionSyntax = PlatformVersionSyntax( 43 | platform: platform, 44 | version: .init( 45 | major: .integerLiteral(major), 46 | components: .init(components) 47 | ) 48 | ) 49 | 50 | return AvailabilityArgumentSyntax(argument: .availabilityVersionRestriction(versionSyntax)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Utils/Messages.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Messages.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 04. 01.. 6 | // 7 | 8 | enum Messages { 9 | static let givenMessage = "Use given(_ service:) instead." 10 | static let whenMessage = "Use when(_ service:) instead." 11 | static let verifyMessage = "Use verify(_ service:) instead." 12 | } 13 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Utils/Namespace.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Namespace.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2024. 03. 28.. 6 | // 7 | 8 | #if canImport(SwiftSyntax600) || swift(<6) 9 | import SwiftSyntax 10 | #else 11 | @preconcurrency import SwiftSyntax 12 | #endif 13 | 14 | // swiftlint:disable identifier_name 15 | enum NS { 16 | static let MOCKING: TokenSyntax = "MOCKING" 17 | 18 | static let generic: TokenSyntax = "generic" 19 | static let value: TokenSyntax = "value" 20 | static let eraseToGenericValue: TokenSyntax = "eraseToGenericValue" 21 | static let newValue: TokenSyntax = "newValue" 22 | static let other: TokenSyntax = "other" 23 | static let match: TokenSyntax = "match" 24 | static let left: TokenSyntax = "left" 25 | static let right: TokenSyntax = "right" 26 | static let with: TokenSyntax = "with" 27 | static let reset: TokenSyntax = "reset" 28 | static let scopes: TokenSyntax = "scopes" 29 | static let all: TokenSyntax = "all" 30 | static let available: TokenSyntax = "available" 31 | static let deprecated: TokenSyntax = "deprecated" 32 | static let message: TokenSyntax = "message" 33 | static let kind: TokenSyntax = "kind" 34 | static let setKind: TokenSyntax = "setKind" 35 | static let any: TokenSyntax = "any" 36 | static let initializer: TokenSyntax = "init" 37 | static let member: TokenSyntax = "member" 38 | static let mock: TokenSyntax = "mock" 39 | static let mockThrowing: TokenSyntax = "mockThrowing" 40 | static let producer: TokenSyntax = "producer" 41 | static let cast: TokenSyntax = "cast" 42 | static let addInvocation: TokenSyntax = "addInvocation" 43 | static let performActions: TokenSyntax = "performActions" 44 | static let policy: TokenSyntax = "policy" 45 | static let error: TokenSyntax = "error" 46 | static let iOS: TokenSyntax = "iOS" 47 | static let macOS: TokenSyntax = "macOS" 48 | static let tvOS: TokenSyntax = "tvOS" 49 | static let watchOS: TokenSyntax = "watchOS" 50 | 51 | static let _given: TokenSyntax = "_given" 52 | static let _when: TokenSyntax = "_when" 53 | static let _verify: TokenSyntax = "_verify" 54 | static let _andSign: String = "&&" 55 | static let _init: TokenSyntax = "init" 56 | static let _star: TokenSyntax = "*" 57 | static let _for: TokenSyntax = "for" 58 | static let get_: String = "get_" 59 | static let set_: String = "set_" 60 | 61 | static let mocker: TokenSyntax = "mocker" 62 | 63 | static let Mockable: TokenSyntax = "Mockable" 64 | static let Mock: TokenSyntax = "Mock" 65 | static let MockableService: TokenSyntax = "MockableService" 66 | static let Bool: TokenSyntax = "Bool" 67 | static let GenericValue: String = "GenericValue" 68 | static let Mocker: TokenSyntax = "Mocker" 69 | static let Builder: TokenSyntax = "Builder" 70 | static let Member: TokenSyntax = "Member" 71 | static let Matchable: TokenSyntax = "Matchable" 72 | static let CaseIdentifiable: TokenSyntax = "CaseIdentifiable" 73 | static let ReturnBuilder: TokenSyntax = "ReturnBuilder" 74 | static let ActionBuilder: TokenSyntax = "ActionBuilder" 75 | static let VerifyBuilder: TokenSyntax = "VerifyBuilder" 76 | static let MockerScope: TokenSyntax = "MockerScope" 77 | static let MockerPolicy: TokenSyntax = "MockerPolicy" 78 | static let Set: TokenSyntax = "Set" 79 | static let Void: TokenSyntax = "Void" 80 | static let Actor: TokenSyntax = "Actor" 81 | static let Error: TokenSyntax = "Error" 82 | static let NSObjectProtocol: String = "NSObjectProtocol" 83 | static let NSObject: TokenSyntax = "NSObject" 84 | static let Swift: TokenSyntax = "Swift" 85 | static let Sendable: TokenSyntax = "Sendable" 86 | 87 | static func Parameter(_ type: String) -> TokenSyntax { "Parameter<\(raw: type)>" } 88 | static func Param(suffix: String) -> TokenSyntax { "Param\(raw: suffix)" } 89 | static func Function(_ kind: BuilderKind) -> TokenSyntax { "Function\(raw: kind.name)" } 90 | static func ThrowingFunction(_ kind: BuilderKind) -> TokenSyntax { "ThrowingFunction\(raw: kind.name)" } 91 | static func Property(_ kind: BuilderKind) -> TokenSyntax { "Property\(raw: kind.name)" } 92 | static func ThrowingProperty(_ kind: BuilderKind) -> TokenSyntax { "ThrowingProperty\(raw: kind.name)" } 93 | } 94 | // swiftlint:enable identifier_name 95 | -------------------------------------------------------------------------------- /Sources/MockableMacro/Utils/TokenFinder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenFinder.swift 3 | // MockableMacro 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 20.. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | class TokenVisitor: SyntaxVisitor { 11 | var match: (TokenSyntax) -> Bool 12 | init(match: @escaping (TokenSyntax) -> Bool) { 13 | self.match = match 14 | super.init(viewMode: .sourceAccurate) 15 | } 16 | override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind { 17 | match(token) ? .skipChildren : .visitChildren 18 | } 19 | } 20 | 21 | enum TokenFinder { 22 | static func find( 23 | in syntax: some SyntaxProtocol, 24 | matching matcher: @escaping (TokenSyntax) -> Bool 25 | ) -> TokenSyntax? { 26 | var result: TokenSyntax? 27 | let visitor = TokenVisitor { 28 | let match = matcher($0) 29 | if match, result == nil { 30 | result = $0 31 | } 32 | return result != nil 33 | } 34 | visitor.walk(syntax) 35 | return result 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/MockableMacroTests/DocCommentsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DocCommentsTests.swift 3 | // 4 | // 5 | // Created by Sagar Dagdu on 20/12/24. 6 | // 7 | 8 | import XCTest 9 | import MacroTesting 10 | 11 | final class DocCommentsTests: MockableMacroTestCase { 12 | func test_documentation_comments() { 13 | assertMacro { 14 | """ 15 | /// Protocol documentation 16 | /// Multiple lines 17 | @Mockable 18 | protocol Test { 19 | var foo: Int { get } 20 | } 21 | """ 22 | } expansion: { 23 | """ 24 | /// Protocol documentation 25 | /// Multiple lines 26 | protocol Test { 27 | var foo: Int { get } 28 | } 29 | 30 | #if MOCKING 31 | /// Protocol documentation 32 | /// Multiple lines 33 | final class MockTest: Test, Mockable.MockableService { 34 | typealias Mocker = Mockable.Mocker 35 | private let mocker = Mocker() 36 | @available(*, deprecated, message: "Use given(_ service:) instead. ") 37 | nonisolated var _given: ReturnBuilder { 38 | .init(mocker: mocker) 39 | } 40 | @available(*, deprecated, message: "Use when(_ service:) instead. ") 41 | nonisolated var _when: ActionBuilder { 42 | .init(mocker: mocker) 43 | } 44 | @available(*, deprecated, message: "Use verify(_ service:) instead. ") 45 | nonisolated var _verify: VerifyBuilder { 46 | .init(mocker: mocker) 47 | } 48 | nonisolated func reset(_ scopes: Set = .all) { 49 | mocker.reset(scopes: scopes) 50 | } 51 | nonisolated init(policy: Mockable.MockerPolicy? = nil) { 52 | if let policy { 53 | mocker.policy = policy 54 | } 55 | } 56 | var foo: Int { 57 | get { 58 | let member: Member = .m1_foo 59 | return mocker.mock(member) { producer in 60 | let producer = try cast(producer) as () -> Int 61 | return producer() 62 | } 63 | } 64 | } 65 | enum Member: Mockable.Matchable, Mockable.CaseIdentifiable, Swift.Sendable { 66 | case m1_foo 67 | func match(_ other: Member) -> Bool { 68 | switch (self, other) { 69 | case (.m1_foo, .m1_foo): 70 | return true 71 | } 72 | } 73 | } 74 | struct ReturnBuilder: Mockable.Builder { 75 | private let mocker: Mocker 76 | init(mocker: Mocker) { 77 | self.mocker = mocker 78 | } 79 | var foo: Mockable.FunctionReturnBuilder Int> { 80 | .init(mocker, kind: .m1_foo) 81 | } 82 | } 83 | struct ActionBuilder: Mockable.Builder { 84 | private let mocker: Mocker 85 | init(mocker: Mocker) { 86 | self.mocker = mocker 87 | } 88 | var foo: Mockable.FunctionActionBuilder { 89 | .init(mocker, kind: .m1_foo) 90 | } 91 | } 92 | struct VerifyBuilder: Mockable.Builder { 93 | private let mocker: Mocker 94 | init(mocker: Mocker) { 95 | self.mocker = mocker 96 | } 97 | var foo: Mockable.FunctionVerifyBuilder { 98 | .init(mocker, kind: .m1_foo) 99 | } 100 | } 101 | } 102 | #endif 103 | """ 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Tests/MockableMacroTests/InheritedTypeMappingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InheritedTypeMappingTests.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 27/09/2024. 6 | // 7 | 8 | import MacroTesting 9 | import XCTest 10 | 11 | final class InheritedTypeMappingTests: MockableMacroTestCase { 12 | func test_nsobject_conformance() { 13 | assertMacro { 14 | """ 15 | @Mockable 16 | protocol TestObject: NSObjectProtocol { 17 | func foo() 18 | } 19 | """ 20 | } expansion: { 21 | """ 22 | protocol TestObject: NSObjectProtocol { 23 | func foo() 24 | } 25 | 26 | #if MOCKING 27 | final class MockTestObject: NSObject, TestObject, Mockable.MockableService { 28 | typealias Mocker = Mockable.Mocker 29 | private let mocker = Mocker() 30 | @available(*, deprecated, message: "Use given(_ service:) instead. ") 31 | nonisolated var _given: ReturnBuilder { 32 | .init(mocker: mocker) 33 | } 34 | @available(*, deprecated, message: "Use when(_ service:) instead. ") 35 | nonisolated var _when: ActionBuilder { 36 | .init(mocker: mocker) 37 | } 38 | @available(*, deprecated, message: "Use verify(_ service:) instead. ") 39 | nonisolated var _verify: VerifyBuilder { 40 | .init(mocker: mocker) 41 | } 42 | nonisolated func reset(_ scopes: Set = .all) { 43 | mocker.reset(scopes: scopes) 44 | } 45 | nonisolated init(policy: Mockable.MockerPolicy? = nil) { 46 | if let policy { 47 | mocker.policy = policy 48 | } 49 | } 50 | func foo() { 51 | let member: Member = .m1_foo 52 | mocker.mock(member) { producer in 53 | let producer = try cast(producer) as () -> Void 54 | return producer() 55 | } 56 | } 57 | enum Member: Mockable.Matchable, Mockable.CaseIdentifiable, Swift.Sendable { 58 | case m1_foo 59 | func match(_ other: Member) -> Bool { 60 | switch (self, other) { 61 | case (.m1_foo, .m1_foo): 62 | return true 63 | } 64 | } 65 | } 66 | struct ReturnBuilder: Mockable.Builder { 67 | private let mocker: Mocker 68 | init(mocker: Mocker) { 69 | self.mocker = mocker 70 | } 71 | func foo() -> Mockable.FunctionReturnBuilder Void> { 72 | .init(mocker, kind: .m1_foo) 73 | } 74 | } 75 | struct ActionBuilder: Mockable.Builder { 76 | private let mocker: Mocker 77 | init(mocker: Mocker) { 78 | self.mocker = mocker 79 | } 80 | func foo() -> Mockable.FunctionActionBuilder { 81 | .init(mocker, kind: .m1_foo) 82 | } 83 | } 84 | struct VerifyBuilder: Mockable.Builder { 85 | private let mocker: Mocker 86 | init(mocker: Mocker) { 87 | self.mocker = mocker 88 | } 89 | func foo() -> Mockable.FunctionVerifyBuilder { 90 | .init(mocker, kind: .m1_foo) 91 | } 92 | } 93 | } 94 | #endif 95 | """ 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Tests/MockableMacroTests/InitRequirementTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InitRequirementTests.swift 3 | // 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 23.. 6 | // 7 | 8 | import MacroTesting 9 | import XCTest 10 | 11 | final class InitRequirementTests: MockableMacroTestCase { 12 | func test_init_requirement() { 13 | assertMacro { 14 | """ 15 | @Mockable 16 | protocol Test { 17 | init() 18 | init(name: String) 19 | } 20 | """ 21 | } expansion: { 22 | """ 23 | protocol Test { 24 | init() 25 | init(name: String) 26 | } 27 | 28 | #if MOCKING 29 | final class MockTest: Test, Mockable.MockableService { 30 | typealias Mocker = Mockable.Mocker 31 | private let mocker = Mocker() 32 | @available(*, deprecated, message: "Use given(_ service:) instead. ") 33 | nonisolated var _given: ReturnBuilder { 34 | .init(mocker: mocker) 35 | } 36 | @available(*, deprecated, message: "Use when(_ service:) instead. ") 37 | nonisolated var _when: ActionBuilder { 38 | .init(mocker: mocker) 39 | } 40 | @available(*, deprecated, message: "Use verify(_ service:) instead. ") 41 | nonisolated var _verify: VerifyBuilder { 42 | .init(mocker: mocker) 43 | } 44 | nonisolated func reset(_ scopes: Set = .all) { 45 | mocker.reset(scopes: scopes) 46 | } 47 | nonisolated init(policy: Mockable.MockerPolicy? = nil) { 48 | if let policy { 49 | mocker.policy = policy 50 | } 51 | } 52 | init() { 53 | } 54 | init(name: String) { 55 | } 56 | enum Member: Mockable.Matchable, Mockable.CaseIdentifiable, Swift.Sendable { 57 | func match(_ other: Member) -> Bool { 58 | switch (self, other) { 59 | } 60 | } 61 | } 62 | struct ReturnBuilder: Mockable.Builder { 63 | private let mocker: Mocker 64 | init(mocker: Mocker) { 65 | self.mocker = mocker 66 | } 67 | } 68 | struct ActionBuilder: Mockable.Builder { 69 | private let mocker: Mocker 70 | init(mocker: Mocker) { 71 | self.mocker = mocker 72 | } 73 | } 74 | struct VerifyBuilder: Mockable.Builder { 75 | private let mocker: Mocker 76 | init(mocker: Mocker) { 77 | self.mocker = mocker 78 | } 79 | } 80 | } 81 | #endif 82 | """ 83 | } 84 | } 85 | 86 | func test_multiple_init_requirement() { 87 | assertMacro { 88 | """ 89 | @Mockable 90 | protocol Test { 91 | init?() async throws 92 | init(name: String) 93 | init(name value: String, _ index: Int) 94 | } 95 | """ 96 | } expansion: { 97 | """ 98 | protocol Test { 99 | init?() async throws 100 | init(name: String) 101 | init(name value: String, _ index: Int) 102 | } 103 | 104 | #if MOCKING 105 | final class MockTest: Test, Mockable.MockableService { 106 | typealias Mocker = Mockable.Mocker 107 | private let mocker = Mocker() 108 | @available(*, deprecated, message: "Use given(_ service:) instead. ") 109 | nonisolated var _given: ReturnBuilder { 110 | .init(mocker: mocker) 111 | } 112 | @available(*, deprecated, message: "Use when(_ service:) instead. ") 113 | nonisolated var _when: ActionBuilder { 114 | .init(mocker: mocker) 115 | } 116 | @available(*, deprecated, message: "Use verify(_ service:) instead. ") 117 | nonisolated var _verify: VerifyBuilder { 118 | .init(mocker: mocker) 119 | } 120 | nonisolated func reset(_ scopes: Set = .all) { 121 | mocker.reset(scopes: scopes) 122 | } 123 | nonisolated init(policy: Mockable.MockerPolicy? = nil) { 124 | if let policy { 125 | mocker.policy = policy 126 | } 127 | } 128 | init?() async throws { 129 | } 130 | init(name: String) { 131 | } 132 | init(name value: String, _ index: Int) { 133 | } 134 | enum Member: Mockable.Matchable, Mockable.CaseIdentifiable, Swift.Sendable { 135 | func match(_ other: Member) -> Bool { 136 | switch (self, other) { 137 | } 138 | } 139 | } 140 | struct ReturnBuilder: Mockable.Builder { 141 | private let mocker: Mocker 142 | init(mocker: Mocker) { 143 | self.mocker = mocker 144 | } 145 | } 146 | struct ActionBuilder: Mockable.Builder { 147 | private let mocker: Mocker 148 | init(mocker: Mocker) { 149 | self.mocker = mocker 150 | } 151 | } 152 | struct VerifyBuilder: Mockable.Builder { 153 | private let mocker: Mocker 154 | init(mocker: Mocker) { 155 | self.mocker = mocker 156 | } 157 | } 158 | } 159 | #endif 160 | """ 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Tests/MockableMacroTests/TypedThrowsTests_Swift6.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypedThrowsTests.swift 3 | // 4 | // 5 | // Created by Kolos Foltanyi on 08/07/2024. 6 | // 7 | 8 | import MacroTesting 9 | import XCTest 10 | import SwiftSyntax 11 | 12 | #if canImport(SwiftSyntax600) 13 | final class TypedThrowsTests_Swift6: MockableMacroTestCase { 14 | func test_typed_throws_requirement() { 15 | assertMacro { 16 | """ 17 | @Mockable 18 | protocol TypedErrorProtocol { 19 | func foo() throws(ExampleError) 20 | var baz: String { get throws(ExampleError) } 21 | } 22 | """ 23 | } expansion: { 24 | """ 25 | protocol TypedErrorProtocol { 26 | func foo() throws(ExampleError) 27 | var baz: String { get throws(ExampleError) } 28 | } 29 | 30 | #if MOCKING 31 | final class MockTypedErrorProtocol: TypedErrorProtocol, Mockable.MockableService { 32 | typealias Mocker = Mockable.Mocker 33 | private let mocker = Mocker() 34 | @available(*, deprecated, message: "Use given(_ service:) instead. ") 35 | nonisolated var _given: ReturnBuilder { 36 | .init(mocker: mocker) 37 | } 38 | @available(*, deprecated, message: "Use when(_ service:) instead. ") 39 | nonisolated var _when: ActionBuilder { 40 | .init(mocker: mocker) 41 | } 42 | @available(*, deprecated, message: "Use verify(_ service:) instead. ") 43 | nonisolated var _verify: VerifyBuilder { 44 | .init(mocker: mocker) 45 | } 46 | nonisolated func reset(_ scopes: Set = .all) { 47 | mocker.reset(scopes: scopes) 48 | } 49 | nonisolated init(policy: Mockable.MockerPolicy? = nil) { 50 | if let policy { 51 | mocker.policy = policy 52 | } 53 | } 54 | func foo() throws(ExampleError) { 55 | let member: Member = .m2_foo 56 | try mocker.mockThrowing(member, error: ExampleError.self) { producer in 57 | let producer = try cast(producer) as () throws -> Void 58 | return try producer() 59 | } 60 | } 61 | var baz: String { 62 | get throws(ExampleError) { 63 | let member: Member = .m1_baz 64 | return try mocker.mockThrowing(member, error: ExampleError.self) { producer in 65 | let producer = try cast(producer) as () throws -> String 66 | return try producer() 67 | } 68 | } 69 | } 70 | enum Member: Mockable.Matchable, Mockable.CaseIdentifiable, Swift.Sendable { 71 | case m1_baz 72 | case m2_foo 73 | func match(_ other: Member) -> Bool { 74 | switch (self, other) { 75 | case (.m1_baz, .m1_baz): 76 | return true 77 | case (.m2_foo, .m2_foo): 78 | return true 79 | default: 80 | return false 81 | } 82 | } 83 | } 84 | struct ReturnBuilder: Mockable.Builder { 85 | private let mocker: Mocker 86 | init(mocker: Mocker) { 87 | self.mocker = mocker 88 | } 89 | var baz: Mockable.ThrowingFunctionReturnBuilder String> { 90 | .init(mocker, kind: .m1_baz) 91 | } 92 | func foo() -> Mockable.ThrowingFunctionReturnBuilder Void> { 93 | .init(mocker, kind: .m2_foo) 94 | } 95 | } 96 | struct ActionBuilder: Mockable.Builder { 97 | private let mocker: Mocker 98 | init(mocker: Mocker) { 99 | self.mocker = mocker 100 | } 101 | var baz: Mockable.ThrowingFunctionActionBuilder { 102 | .init(mocker, kind: .m1_baz) 103 | } 104 | func foo() -> Mockable.ThrowingFunctionActionBuilder { 105 | .init(mocker, kind: .m2_foo) 106 | } 107 | } 108 | struct VerifyBuilder: Mockable.Builder { 109 | private let mocker: Mocker 110 | init(mocker: Mocker) { 111 | self.mocker = mocker 112 | } 113 | var baz: Mockable.ThrowingFunctionVerifyBuilder { 114 | .init(mocker, kind: .m1_baz) 115 | } 116 | func foo() -> Mockable.ThrowingFunctionVerifyBuilder { 117 | .init(mocker, kind: .m2_foo) 118 | } 119 | } 120 | } 121 | #endif 122 | """ 123 | } 124 | } 125 | } 126 | #endif 127 | -------------------------------------------------------------------------------- /Tests/MockableMacroTests/Utils/MockableMacroTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockableMacroTestCase.swift 3 | // 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 21.. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntaxMacros 10 | import SwiftSyntaxMacrosTestSupport 11 | import MacroTesting 12 | import XCTest 13 | 14 | #if canImport(MockableMacro) 15 | import MockableMacro 16 | #endif 17 | 18 | class MockableMacroTestCase: XCTestCase { 19 | override func invokeTest() { 20 | #if canImport(MockableMacro) 21 | withMacroTesting(record: false, macros: ["Mockable": MockableMacro.self]) { 22 | super.invokeTest() 23 | } 24 | #else 25 | fatalError("Macro tests can only be run on the host platform!") 26 | #endif 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/MockableTests/ActionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionTests.swift 3 | // 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 22.. 6 | // 7 | 8 | import XCTest 9 | import Mockable 10 | 11 | final class ActionTests: XCTestCase { 12 | 13 | // MARK: Properties 14 | 15 | private let mock = MockTestService() 16 | 17 | // MARK: Overrides 18 | 19 | override func tearDown() { 20 | mock.reset() 21 | Matcher.reset() 22 | } 23 | 24 | // MARK: Tests 25 | 26 | func test_givenActionRegistered_whenMockCalled_actionIsCalled() throws { 27 | var called = false 28 | 29 | given(mock).getUser(for: .any).willReturn(.test1) 30 | 31 | when(mock).getUser(for: .any).perform { 32 | called = true 33 | } 34 | 35 | _ = try mock.getUser(for: UUID()) 36 | 37 | XCTAssertTrue(called) 38 | } 39 | 40 | func test_givenActionRegisteredForMutableProperty_whenMockCalled_relatedActionsAreCalled() { 41 | var getCalled = false 42 | var setCalled = false 43 | 44 | given(mock).name.willReturn("") 45 | 46 | when(mock) 47 | .name().performOnGet { getCalled = true } 48 | .name().performOnSet { setCalled = true } 49 | 50 | _ = mock.name 51 | mock.name = "" 52 | 53 | XCTAssertTrue(getCalled) 54 | XCTAssertTrue(setCalled) 55 | } 56 | 57 | func test_givenParameterConditionedActions_whenMockCalled_onlyCallsActionWithMatchingParameter() throws { 58 | var firstCalled = false 59 | var secondCalled = false 60 | 61 | let id1 = UUID() 62 | let id2 = UUID() 63 | 64 | given(mock).getUser(for: .any).willReturn(.test1) 65 | 66 | when(mock) 67 | .getUser(for: .value(id1)).perform { firstCalled = true } 68 | .getUser(for: .value(id2)).perform { secondCalled = true } 69 | 70 | _ = try mock.getUser(for: id2) 71 | 72 | XCTAssertFalse(firstCalled) 73 | XCTAssertTrue(secondCalled) 74 | } 75 | 76 | func test_givenMultipleActions_whenMockCalled_allActionsAreCalled() throws { 77 | var firstCalled = false 78 | var secondCalled = false 79 | var thirdCalled = false 80 | 81 | given(mock).getUser(for: .any).willReturn(.test1) 82 | 83 | when(mock) 84 | .getUser(for: .any).perform { firstCalled = true } 85 | .getUser(for: .any).perform { secondCalled = true } 86 | .getUser(for: .any).perform { thirdCalled = true } 87 | 88 | _ = try mock.getUser(for: UUID()) 89 | 90 | XCTAssertTrue(firstCalled) 91 | XCTAssertTrue(secondCalled) 92 | XCTAssertTrue(thirdCalled) 93 | } 94 | 95 | func test_givenGenericActionsRegistered_whenMockCalled_actionWithMatchingTypeCalled() { 96 | var firstCalled = false 97 | var secondCalled = false 98 | var thirdCalled = false 99 | 100 | let id = UUID() 101 | 102 | given(mock) 103 | .delete(for: .value("id")).willReturn(1) 104 | .delete(for: .value(id)).willReturn(2) 105 | 106 | when(mock) 107 | .delete(for: .value("id")).perform { firstCalled = true } 108 | .delete(for: .value(id)).perform { secondCalled = true } 109 | .delete(for: .value("id")).perform { thirdCalled = true } 110 | 111 | _ = mock.delete(for: id) 112 | 113 | XCTAssertFalse(firstCalled) 114 | XCTAssertTrue(secondCalled) 115 | XCTAssertFalse(thirdCalled) 116 | } 117 | 118 | func test_givenGenericParamAndReturnFunc_whenActionUsed_onlyParamIsInfered() { 119 | var called = false 120 | 121 | given(mock) 122 | .retrieveItem(item: Parameter.any) 123 | .willReturn(0) 124 | 125 | when(mock) 126 | .retrieveItem(item: Parameter.any) 127 | .perform { called = true } 128 | 129 | let _: Int = mock.retrieveItem(item: 0) 130 | 131 | XCTAssertTrue(called) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Tests/MockableTests/BuildTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestProtocol.swift 3 | // 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 21.. 6 | // 7 | 8 | import Mockable 9 | 10 | @Mockable 11 | protocol TestAssociatedTypes where Item2: Identifiable { 12 | associatedtype Item1 13 | associatedtype Item2: Equatable, Hashable 14 | associatedtype Item3 where Item3: Equatable, Item3: Hashable 15 | func foo(item1: Item1) -> Item1 16 | func foo(item2: Item2) -> Item2 17 | func foo(item3: Item3) -> Item3 18 | } 19 | 20 | @Mockable 21 | protocol TestExoticParameters { 22 | func modifyValue(_ value: inout Int) 23 | func printValues(_ values: Int...) 24 | func execute(operation: @escaping () throws -> Void) 25 | } 26 | 27 | @Mockable 28 | protocol TestFunctionEffects { 29 | func canThrowError() throws 30 | func returnsAndThrows() throws -> String 31 | func call(operation: @escaping () throws -> Void) rethrows 32 | func asyncFunction() async 33 | func asyncThrowingFunction() async throws 34 | func asyncParamFunction(param: @escaping () async throws -> Void) async 35 | nonisolated func nonisolatedFunction() async 36 | } 37 | 38 | @Mockable 39 | protocol TestGenericFunctions { 40 | func foo(item: (Array<[(Set, String)]>, Int)) 41 | func genericFunc(item: T) -> V 42 | func getInts() -> any Collection 43 | func method( 44 | prop1: T, prop2: E, prop3: C, prop4: I 45 | ) where E: Equatable, E: Hashable, C: Codable 46 | } 47 | 48 | @Mockable 49 | protocol TestNameCollisions { 50 | func fetchData(for name: Int) -> String 51 | func fetchData(for name: String) -> String 52 | func fetchData(forA name: String) -> String 53 | func fetchData(forB name: String) -> String 54 | func `repeat`(param: Bool) -> Int 55 | } 56 | 57 | @Mockable 58 | protocol TestPropertyRequirements { 59 | var computedInt: Int { get } 60 | var computedString: String { get } 61 | var mutableInt: Int { get set } 62 | var mutableUnwrappedString: String! { get set } 63 | var throwingProperty: Int { get throws } 64 | var asyncProperty: String { get async } 65 | var asyncThrowingProperty: String { get async throws } 66 | nonisolated var nonisolatedProperty: String { get set } 67 | } 68 | 69 | @Mockable 70 | protocol TestInitRequirements { 71 | init?() async throws 72 | init(index: Int) 73 | init(name value: String, index: Int) 74 | } 75 | 76 | @Mockable 77 | protocol TestAttributes { 78 | @available(iOS 16, *) 79 | init(attributed: String) 80 | @available(iOS 16, *) 81 | var attributedProp: Int { get } 82 | @available(iOS 16, *) 83 | func attributedTest() 84 | } 85 | 86 | #if canImport(Foundation) 87 | import Foundation 88 | 89 | @Mockable 90 | protocol TestNSObject: NSObjectProtocol { 91 | func foo(param: Int) -> String 92 | } 93 | #endif 94 | 95 | @Mockable 96 | protocol TestActorConformance: Actor { 97 | func foo(param: Int) -> String 98 | } 99 | 100 | @Mockable 101 | @MainActor 102 | protocol TestGlobalActor { 103 | func foo(param: Int) -> String 104 | } 105 | 106 | @Mockable 107 | protocol TestSendable: Sendable { 108 | func foo(param: Int) -> String 109 | } 110 | 111 | @Mockable 112 | public protocol TestReservedKeyword { 113 | func example(for: String) async throws -> String 114 | } 115 | -------------------------------------------------------------------------------- /Tests/MockableTests/GivenTests_Swift6.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GivenTests_Swift6.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 28/09/2024. 6 | // 7 | 8 | import XCTest 9 | import Mockable 10 | 11 | #if swift(>=6) 12 | final class GivenTests_Swift6: XCTestCase { 13 | 14 | // MARK: Properties 15 | 16 | private var mock = MockTestService_Swift6() 17 | 18 | // MARK: Overrides 19 | 20 | override func tearDown() { 21 | mock.reset() 22 | Matcher.reset() 23 | } 24 | 25 | // MARK: Tests 26 | 27 | func test_givenTypedThrows_whenErrorSet_correctTypeThrown() { 28 | given(mock) 29 | .fetch().willThrow(.notFound) 30 | .fetched.willThrow(.notFound) 31 | 32 | do { 33 | try mock.fetch() 34 | } catch { 35 | XCTAssertEqual(error, UserError.notFound) 36 | } 37 | do { 38 | _ = try mock.fetched 39 | } catch { 40 | XCTAssertEqual(error, UserError.notFound) 41 | } 42 | } 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /Tests/MockableTests/Helpers/Task+Sleep.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Task+Sleep.swift 3 | // 4 | // 5 | // Created by Kolos Foltanyi on 2024. 04. 07.. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Task where Success == Never, Failure == Never { 11 | static func sleep(seconds: TimeInterval) async throws { 12 | try await sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/MockableTests/PolicyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PolicyTests.swift 3 | // 4 | // 5 | // Created by Kolos Foltanyi on 2024. 04. 02.. 6 | // 7 | 8 | import XCTest 9 | import Mockable 10 | 11 | public struct Car: Equatable { 12 | var name: String 13 | var seats: Int 14 | } 15 | 16 | extension Car: Mocked { 17 | public static var mock: Car { 18 | Car(name: "Mock", seats: 4) 19 | } 20 | } 21 | 22 | @Mockable 23 | private protocol PolicyService { 24 | func throwingVoidFunc() throws 25 | var throwingVoidProp: Void { get throws } 26 | func nonThrowingVoidFunc() 27 | var nonThrowingVoidProp: Void { get } 28 | func optionalFunc() -> String? 29 | var optionalProp: String? { get } 30 | func carFunc() -> Car 31 | func optionalCarFunc() -> Car? 32 | var carProp: Car { get } 33 | func carsFunc() -> [Car] 34 | var carsProp: [Car] { get } 35 | } 36 | 37 | final class PolicyTests: XCTestCase { 38 | 39 | // MARK: Overrides 40 | 41 | override func tearDown() { 42 | Matcher.reset() 43 | MockerPolicy.default = .strict 44 | } 45 | 46 | // MARK: Tests 47 | 48 | func test_whenDefaultPolicyChanged_allCallsAreRelaxed() throws { 49 | let mock = MockPolicyService() 50 | MockerPolicy.default = .relaxed 51 | try testRelaxed(on: mock) 52 | } 53 | 54 | func test_whenCustomRelaxedPolicySet_allCallsAreRelaxed() throws { 55 | let mock = MockPolicyService(policy: .relaxed) 56 | try testRelaxed(on: mock) 57 | } 58 | 59 | func test_whenCustomVoidPolicySet_mockReturnsDefault() throws { 60 | let mock = MockPolicyService(policy: .relaxedVoid) 61 | try mock.throwingVoidFunc() 62 | try mock.throwingVoidProp 63 | mock.nonThrowingVoidFunc() 64 | mock.nonThrowingVoidProp 65 | } 66 | 67 | func test_whenOnlyOptionalPolicySet_mockReturnsNilNotMockableValue() throws { 68 | let mock = MockPolicyService(policy: .relaxedOptional) 69 | XCTAssertNil(mock.optionalCarFunc()) 70 | } 71 | 72 | func test_whenCustomMockedPolicySet_mockReturnsDefault() throws { 73 | let mock = MockPolicyService(policy: .relaxedMocked) 74 | XCTAssertEqual(Car.mock, mock.carFunc()) 75 | XCTAssertEqual(Car.mock, mock.optionalCarFunc()) 76 | XCTAssertEqual(Car.mock, mock.carProp) 77 | XCTAssertEqual(Car.mocks, mock.carsFunc()) 78 | XCTAssertEqual(Car.mocks, mock.carsProp) 79 | } 80 | 81 | private func testRelaxed(on service: MockPolicyService) throws { 82 | try service.throwingVoidFunc() 83 | try service.throwingVoidProp 84 | service.nonThrowingVoidFunc() 85 | service.nonThrowingVoidProp 86 | XCTAssertEqual(nil, service.optionalFunc()) 87 | XCTAssertEqual(nil, service.optionalProp) 88 | XCTAssertEqual(Car.mock, service.carFunc()) 89 | XCTAssertEqual(Car.mock, service.optionalCarFunc()) 90 | XCTAssertEqual(Car.mock, service.carProp) 91 | XCTAssertEqual(Car.mocks, service.carsFunc()) 92 | XCTAssertEqual(Car.mocks, service.carsProp) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Tests/MockableTests/Protocols/Models/Product.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Product.swift 3 | // 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 26.. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Product { 11 | var id = UUID() 12 | var name: String 13 | 14 | static let test = Product(name: "product1") 15 | } 16 | -------------------------------------------------------------------------------- /Tests/MockableTests/Protocols/Models/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 26.. 6 | // 7 | 8 | import Foundation 9 | 10 | struct User: Equatable, Hashable { 11 | let id = UUID() 12 | let name: String 13 | var age: Int 14 | 15 | static let test1: User = .init(name: "test1", age: 1) 16 | static let test2: User = .init(name: "test2", age: 2) 17 | static let test3: User = .init(name: "test3", age: 3) 18 | static let list: [User] = [ 19 | .init(name: "test1", age: 1), 20 | .init(name: "test2", age: 2), 21 | .init(name: "test3", age: 3), 22 | .init(name: "test4", age: 4) 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /Tests/MockableTests/Protocols/Models/UserError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserError.swift 3 | // 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 26.. 6 | // 7 | 8 | enum UserError: Error { 9 | case notFound 10 | } 11 | -------------------------------------------------------------------------------- /Tests/MockableTests/Protocols/TestService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestService.swift 3 | // 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 24.. 6 | // 7 | 8 | import Mockable 9 | import Foundation 10 | 11 | @Mockable 12 | protocol TestService { 13 | 14 | // MARK: Associated Type 15 | 16 | associatedtype Color 17 | func getFavoriteColor() -> Color 18 | 19 | // MARK: Properties 20 | 21 | var name: String { get set } 22 | var computed: String { get } 23 | 24 | // MARK: Functions 25 | 26 | func getUser(for id: UUID) throws -> User 27 | func getUsers(for ids: UUID...) -> [User] 28 | func setUser(user: User) async throws -> Bool 29 | func modify(user: User) -> Int 30 | func update(products: [Product]) -> Int 31 | func download(completion: @escaping (Product) -> Void) 32 | func print() throws 33 | func change(user: inout User) 34 | 35 | // MARK: Generics 36 | 37 | func getUserAndValue(for id: UUID, value: Value) -> (User, Value) 38 | func delete(for value: T) -> Int 39 | func retrieve() -> V 40 | func retrieveItem(item: T) -> V 41 | } 42 | -------------------------------------------------------------------------------- /Tests/MockableTests/Protocols/TestService_Swift6.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestService6.swift 3 | // Mockable 4 | // 5 | // Created by Kolos Foltanyi on 28/09/2024. 6 | // 7 | 8 | import Mockable 9 | 10 | #if swift(>=6) 11 | @Mockable 12 | protocol TestService_Swift6 { 13 | // MARK: Typed Throws 14 | 15 | func fetch() throws(UserError) 16 | var fetched: User { get throws(UserError) } 17 | } 18 | #endif 19 | -------------------------------------------------------------------------------- /Tests/MockableTests/VerifyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VerifyTests.swift 3 | // 4 | // 5 | // Created by Kolos Foltanyi on 2023. 11. 22.. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | @testable import Mockable 11 | 12 | final class VerifyTests: XCTestCase { 13 | 14 | // MARK: Properties 15 | 16 | private let mock = MockTestService() 17 | 18 | // MARK: Overrides 19 | 20 | override func tearDown() { 21 | mock.reset() 22 | Matcher.reset() 23 | } 24 | 25 | // MARK: Tests 26 | 27 | func test_givenMockFunctionIsCalled_whenCountVerified_assertsMatchingCounts() throws { 28 | given(mock).getUser(for: .any).willReturn(.test1) 29 | 30 | _ = try mock.getUser(for: UUID()) 31 | 32 | verify(mock) 33 | .getUser(for: .any).called(.once) 34 | .getUser(for: .any).called(.atLeastOnce) 35 | .getUser(for: .any).called(.less(than: 2)) 36 | .getUser(for: .any).called(.more(than: 0)) 37 | .getUser(for: .any).called(.moreOrEqual(to: 1)) 38 | .getUser(for: .any).called(.lessOrEqual(to: 1)) 39 | .getUser(for: .any).called(.from(0, to: 2)) 40 | .getUser(for: .any).called(.exactly(1)) 41 | } 42 | 43 | @MainActor 44 | func test_givenMockFunctionIsCalledAsyncrhonously_whenCountVerified_assertsMatchingCounts() async { 45 | given(mock).getUser(for: .any).willReturn(.test1) 46 | 47 | Task { 48 | try await Task.sleep(seconds: 0.5) 49 | _ = try mock.getUser(for: UUID()) 50 | } 51 | 52 | verify(mock).getUser(for: .any).called(.never) 53 | 54 | await verify(mock) 55 | .getUser(for: .any).calledEventually(.once) 56 | .getUser(for: .any).calledEventually(.atLeastOnce) 57 | .getUser(for: .any).calledEventually(.less(than: 2)) 58 | .getUser(for: .any).calledEventually(.more(than: 0)) 59 | .getUser(for: .any).calledEventually(.moreOrEqual(to: 1)) 60 | .getUser(for: .any).calledEventually(.lessOrEqual(to: 1)) 61 | .getUser(for: .any).calledEventually(.from(0, to: 2)) 62 | .getUser(for: .any).calledEventually(.exactly(1)) 63 | } 64 | 65 | func test_givenMockPropertyAccessed_whenCountVerified_assertsGetterAndSetter() throws { 66 | let testName = "Name" 67 | 68 | given(mock).name.willReturn(testName) 69 | 70 | _ = mock.name 71 | _ = mock.name 72 | mock.name = testName 73 | 74 | verify(mock) 75 | .name().getCalled(2) 76 | .name().setCalled(.once) 77 | } 78 | 79 | @MainActor 80 | func test_givenMockPropertyAccessedAsynchronously_whenCountVerified_assertsGetterAndSetter() async { 81 | let testName = "Name" 82 | 83 | given(mock).name.willReturn(testName) 84 | 85 | Task { 86 | try await Task.sleep(seconds: 0.5) 87 | _ = mock.name 88 | _ = mock.name 89 | mock.name = testName 90 | } 91 | 92 | verify(mock) 93 | .name().getCalled(.never) 94 | .name().setCalled(.never) 95 | 96 | await verify(mock) 97 | .name().getCalledEventually(.exactly(2)) 98 | .name().setCalledEventually(.once) 99 | } 100 | 101 | func test_givenGenericParamAndReturnFunc_whenVerifyUsed_OnlyParamIsInferred() { 102 | given(mock) 103 | .retrieveItem(item: Parameter.any) 104 | .willReturn(0) 105 | 106 | let _: Int = mock.retrieveItem(item: 0) 107 | 108 | verify(mock) 109 | .retrieveItem(item: Parameter.any) 110 | .called(.atLeastOnce) 111 | } 112 | 113 | @MainActor 114 | func test_givenAsyncVerification_whenSatisfied_verifiesEarlyBeforeTimeout() async throws { 115 | given(mock).getUser(for: .any).willReturn(.test1) 116 | 117 | Task { 118 | try await Task.sleep(for: .seconds(1)) 119 | _ = try self.mock.getUser(for: UUID()) 120 | } 121 | 122 | let verify = verify(mock) 123 | 124 | try await withTimeout(after: 3) { 125 | await verify.getUser(for: .any) 126 | .calledEventually(.atLeastOnce, before: .seconds(5)) 127 | } 128 | } 129 | } 130 | --------------------------------------------------------------------------------