├── .github ├── funding.yml ├── workflows │ ├── swiftlint.yml │ ├── documentation.yml │ └── ci-swiftpm.yml ├── dependabot.yml ├── PULL_REQUEST_TEMPLATE └── ISSUE_TEMPLATE ├── .gitignore ├── Sources └── Fakes │ ├── EmptyError.swift │ ├── Fakes.docc │ ├── Tutorials │ │ └── WritingFakes │ │ │ ├── Resources │ │ │ └── code │ │ │ │ └── RecipeService │ │ │ │ ├── NetworkInterface-01.swift │ │ │ │ ├── NetworkInterface-02.swift │ │ │ │ ├── NetworkInterface-03.swift │ │ │ │ ├── FakeNetworkInterface-01.swift │ │ │ │ ├── FakeNetworkInterface-04.swift │ │ │ │ ├── RecipeService-01.swift │ │ │ │ ├── RecipeService-02.swift │ │ │ │ ├── RecipeService-03.swift │ │ │ │ ├── FakeNetworkInterface-03.swift │ │ │ │ ├── FakeNetworkInterface-02.swift │ │ │ │ ├── RecipeServiceTests-03.swift │ │ │ │ ├── RecipeServiceTests-02.swift │ │ │ │ └── RecipeServiceTests-01.swift │ │ │ ├── WritingFakes.tutorial │ │ │ └── RecipeService.tutorial │ ├── Fakes.md │ └── Guides │ │ ├── NimbleIntegration.md │ │ ├── VerifyingCallbacks.md │ │ └── DependencyInjection.md │ ├── PrivacyInfo.xcprivacy │ ├── Spy │ ├── Spy+AsyncSequence.swift │ ├── Spy+Testing.swift │ ├── Spy+Pendable.swift │ ├── Spy+Result.swift │ ├── Spy.swift │ ├── Spy+ThrowingPendable.swift │ └── Spy+Nimble.swift │ ├── Pendable │ ├── PendableDefaults.swift │ └── Pendable.swift │ ├── PropertySpy │ └── PropertySpy.swift │ └── DynamicResult.swift ├── .swiftlint.yml ├── script ├── build_docs └── release ├── Package.swift ├── Tests └── FakesTests │ ├── PendableTests.swift │ ├── SpyTests+Nimble.swift │ ├── DynamicResultTests.swift │ ├── SpyTests+TestHelpers.swift │ ├── PropertySpyTests.swift │ └── SpyTests.swift ├── Package.resolved ├── README.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── LICENSE /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: [younata] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm 7 | .netrc 8 | 9 | docs/ 10 | -------------------------------------------------------------------------------- /Sources/Fakes/EmptyError.swift: -------------------------------------------------------------------------------- 1 | /// An error that can be used as a default error in tests 2 | public struct EmptyError: Error, Sendable { 3 | public init() {} 4 | } 5 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Tutorials/WritingFakes/Resources/code/RecipeService/NetworkInterface-01.swift: -------------------------------------------------------------------------------- 1 | protocol NetworkInterface { 2 | func get(from url: URL) -> Data 3 | func post(data: Data, to url: URL) 4 | } 5 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Tutorials/WritingFakes/Resources/code/RecipeService/NetworkInterface-02.swift: -------------------------------------------------------------------------------- 1 | protocol NetworkInterface { 2 | func get(from url: URL) throws -> Data 3 | func post(data: Data, to url: URL) throws 4 | } 5 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | - Tests 4 | 5 | disabled_rules: 6 | opt_in_rules: 7 | - yoda_condition 8 | 9 | line_length: 10 | warning: 160 11 | error: 240 12 | 13 | trailing_comma: 14 | mandatory_comma: true 15 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Tutorials/WritingFakes/Resources/code/RecipeService/NetworkInterface-03.swift: -------------------------------------------------------------------------------- 1 | protocol NetworkInterface { 2 | func get(from url: URL) async throws -> Data 3 | func post(data: Data, to url: URL) async throws 4 | } 5 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Tutorials/WritingFakes/Resources/code/RecipeService/FakeNetworkInterface-01.swift: -------------------------------------------------------------------------------- 1 | final class FakeNetworkInterface: NetworkInterface { 2 | func get(from url: URL) -> Data { 3 | Data() 4 | } 5 | 6 | func post(data: Data, to url: URL) {} 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Fakes/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/swiftlint.yml: -------------------------------------------------------------------------------- 1 | name: SwiftLint 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '.github/workflows/swiftlint.yml' 7 | - '.swiftlint.yml' 8 | - '**/*.swift' 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v6 15 | - uses: norio-nomura/action-swiftlint@3.2.1 16 | -------------------------------------------------------------------------------- /script/build_docs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | export DOCC_JSON_PRETTYPRINT="YES" 6 | 7 | mkdir -p docs 8 | 9 | swift package --allow-writing-to-directory docs \ 10 | generate-documentation --target Fakes \ 11 | --disable-indexing \ 12 | --transform-for-static-hosting \ 13 | --hosting-base-path 'swift-fakes' \ 14 | --output-path docs 15 | -------------------------------------------------------------------------------- /Sources/Fakes/Spy/Spy+AsyncSequence.swift: -------------------------------------------------------------------------------- 1 | extension Spy { 2 | public func record(_ sequence: AS) async where Arguments == Result, Returning == Void { 3 | do { 4 | for try await value in sequence { 5 | self(.success(value)) 6 | } 7 | } catch { 8 | self(.failure(error)) 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Tutorials/WritingFakes/WritingFakes.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorials(name: "Writing Fakes") { 2 | @Intro(title: "Creating a Fake") { 3 | A tutorial showing you how to create and use a Fake, within the context 4 | of testing network calls. 5 | } 6 | 7 | @Chapter(name: "Creating and Testing RecipeService") { 8 | @TutorialReference(tutorial: "doc:RecipeService") 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "swift" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | The PR should summarize what was changed and why. Here are some questions to 2 | help you if you're not sure: 3 | 4 | - What behavior was changed? 5 | - What code was refactored / updated to support this change? 6 | - What issues are related to this PR? Or why was this change introduced? 7 | 8 | Checklist - While not every PR needs it, new features should consider this list: 9 | 10 | - [ ] Does this have tests? 11 | - [ ] Does this have documentation? 12 | - [ ] Does this break the public API (Requires major version bump)? 13 | - [ ] Is this a new feature (Requires minor version bump)? 14 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Build Documentation 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - "*" 8 | pull_request: 9 | branches: 10 | - "*" 11 | 12 | permissions: 13 | contents: write 14 | 15 | jobs: 16 | build-documentation: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v6 21 | 22 | - name: Build Docs 23 | run: | 24 | ./script/build_docs 25 | 26 | - name: Deploy Docs 27 | if: github.ref == 'refs/heads/main' 28 | uses: JamesIves/github-pages-deploy-action@v4 29 | with: 30 | folder: docs 31 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Tutorials/WritingFakes/Resources/code/RecipeService/FakeNetworkInterface-04.swift: -------------------------------------------------------------------------------- 1 | final class FakeNetworkInterface: NetworkInterface { 2 | // a `ThrowingPendableSpy` is just a typealias for `Spy<..., ThrowingPendable<..., ...>>`. 3 | // Thus, it is still used exactly same way that `Spy` is. 4 | let getSpy = ThrowingPendableSpy() // PendableSpy and 5 | // ThrowingPendableSpy default to stub with `.pending`. 6 | func get(from url: URL) throws -> Data { 7 | try await getSpy(url) 8 | } 9 | 10 | let postSpy = ThrowingPendableSpy<(data: Data, url: URL), Void, Error>() 11 | func post(data: Data, to url: URL) throws { 12 | try await postSpy((data, url)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Tutorials/WritingFakes/Resources/code/RecipeService/RecipeService-01.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Recipe: Equatable, Codable { 4 | // ... 5 | } 6 | 7 | struct RecipeService { 8 | let networkInterface: NetworkInterface 9 | 10 | func recipes() throws -> [Recipe] { 11 | let url = URL(string: "https://example.com/recipes")! 12 | let data = networkInterface.get(from: url) 13 | return try JSONDecoder().decode([Recipe].self, from: data) 14 | } 15 | 16 | func store(recipe: Recipe) throws { 17 | let url = URL(string: "https://example.com/recipes/store")! 18 | let data = try JSONEncoder().encode(recipe) 19 | networkInterface.post(data: data, to: url) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Tutorials/WritingFakes/Resources/code/RecipeService/RecipeService-02.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Recipe: Equatable, Codable { 4 | // ... 5 | } 6 | 7 | struct RecipeService { 8 | let networkInterface: NetworkInterface 9 | 10 | func recipes() throws -> [Recipe] { 11 | let url = URL(string: "https://example.com/recipes")! 12 | let data = try networkInterface.get(from: url) 13 | return try JSONDecoder().decode([Recipe].self, from: data) 14 | } 15 | 16 | func store(recipe: Recipe) throws { 17 | let url = URL(string: "https://example.com/recipes/store")! 18 | let data = try JSONEncoder().encode(recipe) 19 | try networkInterface.post(data: data, to: url) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Tutorials/WritingFakes/Resources/code/RecipeService/RecipeService-03.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Recipe: Equatable, Codable { 4 | // ... 5 | } 6 | 7 | struct RecipeService { 8 | let networkInterface: NetworkInterface 9 | 10 | func recipes() async throws -> [Recipe] { 11 | let url = URL(string: "https://example.com/recipes")! 12 | let data = try await networkInterface.get(from: url) 13 | return try JSONDecoder().decode([Recipe].self, from: data) 14 | } 15 | 16 | func store(recipe: Recipe) async throws { 17 | let url = URL(string: "https://example.com/recipes/store")! 18 | let data = try JSONEncoder().encode(recipe) 19 | try await networkInterface.post(data: data, to: url) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Tutorials/WritingFakes/Resources/code/RecipeService/FakeNetworkInterface-03.swift: -------------------------------------------------------------------------------- 1 | final class FakeNetworkInterface: NetworkInterface { 2 | // a `ThrowingSpy` is just a typealias for `Spy<..., Result<..., ...>>`. 3 | // Thus, it is still used exactly same way that `Spy` is. 4 | let getSpy = ThrowingSpy(Data()) 5 | func get(from url: URL) throws -> Data { 6 | try getSpy(url) // This now uses the overload of `callAsFunction` to one 7 | // that can either return `Success` (in this case, `Data`) or throw an error 8 | } 9 | 10 | let postSpy = ThrowingSpy<(data: Data, url: URL), Void, Error>() 11 | func post(data: Data, to url: URL) throws { 12 | return try postSpy((data, url)) // the return statement here is necessary 13 | // to force swift to use the correct form of Spy's `callAsFunction`. 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Fakes.md: -------------------------------------------------------------------------------- 1 | # ``Fakes`` 2 | 3 | **Swift Fakes** is an open source collection of Test Doubles for Swift 4 | 5 | ## Overview 6 | 7 | Fakes aims to provide easy-to-use and powerful test double infrastructure for 8 | Swift. 9 | 10 | Currently, Fakes offers the ``Spy`` object. 11 | 12 | ## Terms 13 | 14 | - Term Test Double: Any object used to replace a production object for testing 15 | purposes. 16 | - Term Fake: A non-production implementation of a protocol. 17 | - Term Spy: A test double that records calls to a method, and returns a 18 | preconfigured response. 19 | - Term Subject: A generic term for the object being tested. See [Test Double's 20 | description](https://github.com/testdouble/contributing-tests/wiki/Subject). 21 | 22 | ## Topics 23 | 24 | ### Guides 25 | 26 | 27 | 28 | 29 | ### Tutorials 30 | 31 | 32 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "swift-fakes", 7 | platforms: [ 8 | .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .visionOS(.v1) 9 | ], 10 | products: [ 11 | .library( 12 | name: "Fakes", 13 | targets: ["Fakes"] 14 | ), 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/Quick/Nimble.git", from: "14.0.0"), 18 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), 19 | ], 20 | targets: [ 21 | .target( 22 | name: "Fakes", 23 | dependencies: ["Nimble"], 24 | resources: [ 25 | .copy("PrivacyInfo.xcprivacy") 26 | ] 27 | ), 28 | .testTarget( 29 | name: "FakesTests", 30 | dependencies: ["Fakes", "Nimble"] 31 | ), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Tutorials/WritingFakes/Resources/code/RecipeService/FakeNetworkInterface-02.swift: -------------------------------------------------------------------------------- 1 | final class FakeNetworkInterface: NetworkInterface { 2 | // by convention, the name of a spy is the first part of the method name, 3 | // followed by Spy. 4 | let getSpy = Spy(Data()) // Spies that do not return Void or 5 | // Pendable must be initialized with a default value. 6 | func get(from url: URL) -> Data { 7 | getSpy(url) 8 | } 9 | 10 | let postSpy = Spy<(data: Data, url: URL), Void>() // The first type for 11 | // a Spy is either a tuple of the arguments to the method, or the singular 12 | // argument to the method. When you use a tuple, it is most helpful to 13 | // create a named tuple. We will see more when we write the test. 14 | func post(data: Data, to url: URL) { 15 | postSpy((data, url)) // Swift allows us to pass an unnamed tuple in 16 | // place of a named tuple with the same types. 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Fakes/Pendable/PendableDefaults.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Default values for use with Pendable. 4 | public final class PendableDefaults: @unchecked Sendable { 5 | public static let shared = PendableDefaults() 6 | private let lock = NSLock() 7 | 8 | public init() {} 9 | 10 | /// The amount of time to delay before resolving a pending Pendable with the fallback value. 11 | /// By default this is 2 seconds. Conveniently, just long enough to be twice Nimble's default polling timeout. 12 | /// In general, you should keep this set to some number greater than Nimble's default polling timeout, 13 | /// in order to allow polling matchers to work correctly. 14 | public static var delay: TimeInterval { 15 | get { 16 | PendableDefaults.shared.delay 17 | } 18 | set { 19 | PendableDefaults.shared.delay = newValue 20 | } 21 | } 22 | 23 | private var _delay: TimeInterval = 2 24 | public var delay: TimeInterval { 25 | get { 26 | lock.lock() 27 | defer { lock.unlock() } 28 | return _delay 29 | } 30 | set { 31 | lock.lock() 32 | _delay = newValue 33 | lock.unlock() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | - [ ] I have read [CONTRIBUTING](https://github.com/Quick/swift-fakes/blob/main/CONTRIBUTING.md) and have done my best to follow them. 2 | 3 | ### What did you do? 4 | 5 | Please replace this with what you did. 6 | 7 | ### What did you expect to happen? 8 | 9 | Please replace this with what you expected to happen. 10 | 11 | ### What actually happened instead? 12 | 13 | Please replace this with what happened instead. 14 | 15 | ### Environment 16 | 17 | List the software versions you're using: 18 | 19 | - Quick: *?.?.?* 20 | - Nimble: *?.?.?* 21 | - Swift Fakes: *?.?.?* 22 | - Xcode Version: *?.? (????)* (Open Xcode; In menubar: Xcode > About Xcode) 23 | - Swift Version: *?.?* (Open Xcode Preferences; Components > Toolchains. If none, use `Xcode Default`.) 24 | 25 | Please also mention which package manager you used and its version. Delete the 26 | other package managers in this list: 27 | 28 | - Swift Package Manager *?.?.? (swiftpm-???)* (Use `swift build --version` in Terminal) 29 | 30 | ### Project that demonstrates the issue 31 | 32 | Please link to a project we can download that reproduces the issue. Feel free 33 | to delete this section if it's not relevant to the issue (eg - feature request). 34 | 35 | The project should be [short, self-contained, and correct example](http://sscce.org/). 36 | -------------------------------------------------------------------------------- /Tests/FakesTests/PendableTests.swift: -------------------------------------------------------------------------------- 1 | import Fakes 2 | import Nimble 3 | import XCTest 4 | 5 | final class PendableTests: XCTestCase { 6 | func testSingleCall() async throws { 7 | let subject = Pendable.pending(fallback: 0) 8 | 9 | async let result = subject.call() 10 | 11 | try await Task.sleep(nanoseconds: UInt64(0.01 * 1_000_000_000)) 12 | 13 | subject.resolve(with: 2) 14 | 15 | let value = await result 16 | expect(value).to(equal(2)) 17 | } 18 | 19 | func testMultipleCalls() async throws { 20 | let subject = Pendable.pending(fallback: 0) 21 | 22 | async let result = withTaskGroup(of: Int.self, returning: [Int].self) { taskGroup in 23 | for _ in 0..<100 { 24 | taskGroup.addTask { await subject.call() } 25 | } 26 | 27 | var results = [Int]() 28 | for await value in taskGroup { 29 | results.append(value) 30 | } 31 | return results 32 | } 33 | 34 | try await Task.sleep(nanoseconds: UInt64(0.1 * 1_000_000_000)) 35 | 36 | subject.resolve(with: 3) 37 | 38 | let value = await result 39 | expect(value).to(equal(Array(repeating: 3, count: 100))) 40 | } 41 | 42 | func testAutoresolve() async { 43 | let subject = Pendable.pending(fallback: 3) 44 | 45 | await waitUntil(timeout: .milliseconds(500)) { done in 46 | Task { 47 | _ = await subject.call(fallbackDelay: 0.1) 48 | done() 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "5bc9c42a661634bd36bc39a230eb3807665c4762f9170c426c649f738d2edde6", 3 | "pins" : [ 4 | { 5 | "identity" : "cwlcatchexception", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/mattgallagher/CwlCatchException.git", 8 | "state" : { 9 | "revision" : "07b2ba21d361c223e25e3c1e924288742923f08c", 10 | "version" : "2.2.1" 11 | } 12 | }, 13 | { 14 | "identity" : "cwlpreconditiontesting", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", 17 | "state" : { 18 | "revision" : "0139c665ebb45e6a9fbdb68aabfd7c39f3fe0071", 19 | "version" : "2.2.2" 20 | } 21 | }, 22 | { 23 | "identity" : "nimble", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/Quick/Nimble.git", 26 | "state" : { 27 | "revision" : "035b88ad6ae8035f5ce2b50b0a6d69c3b16d2120", 28 | "version" : "14.0.0" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-docc-plugin", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/apple/swift-docc-plugin", 35 | "state" : { 36 | "revision" : "3e4f133a77e644a5812911a0513aeb7288b07d06", 37 | "version" : "1.4.5" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-docc-symbolkit", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/swiftlang/swift-docc-symbolkit", 44 | "state" : { 45 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", 46 | "version" : "1.0.0" 47 | } 48 | } 49 | ], 50 | "version" : 3 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swift-fakes 2 | 3 | Swift Fakes aims to improve the testability of Swift by providing standardized 4 | [test doubles](https://martinfowler.com/bliki/TestDouble.html). 5 | 6 | Test doubles are objects used to replace production objects for testing purposes. 7 | 8 | ## Installation 9 | 10 | To use the `Fakes` library in a SwiftPM project, add the following line to the 11 | dependencies in your `Package.swift` file: 12 | 13 | ```swift 14 | .package(url: "https://github.com/Quick/swift-fakes", from: "0.1.1"), 15 | ``` 16 | 17 | Include `"Fakes"` as a dependency for your test target: 18 | 19 | ```swift 20 | .testTarget(name: "", dependencies: [ 21 | .product(name: "Fakes", package: "swift-fakes"), 22 | ]), 23 | ``` 24 | 25 | ## Motivation 26 | 27 | When writing tests, we want to write one thing at a time. This is best done by 28 | providing _fakes_ or non-production test-controllable objects to the thing being 29 | tested (the [subject](https://github.com/testdouble/contributing-tests/wiki/Subject)). 30 | This is typically done by writing fakes that implement the protocols that the 31 | subject depends on. Swift Fakes aims to make writing Fakes as easy as possible. 32 | 33 | ## Contents 34 | 35 | For the time being, Swift Fakes only offers the `Spy` object. `Spy`s are a kind 36 | of test double that record calls to the object, and return a preset response. 37 | 38 | ### Spy 39 | 40 | Spies are meant to be used in Fake objects to record arguments to a call, and 41 | return pre-stubbed responses. 42 | 43 | See the [documentation](https://quick.github.io/swift-fakes/documentation/fakes). 44 | 45 | ## Source Stability 46 | 47 | Swift Fakes is currently available as an Alpha. By 1.0, we aim to have it source 48 | stable and following Semantic Versioning. 49 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Tutorials/WritingFakes/Resources/code/RecipeService/RecipeServiceTests-03.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class RecipeServiceTests: XCTestCase { 4 | var networkInterface: FakeNetworkInterface! 5 | var subject: RecipeService! 6 | 7 | override func setUp() { 8 | super.setUp() 9 | 10 | networkInterface = FakeNetworkInterface() 11 | subject = RecipeService(networkInterface: networkInterface) 12 | } 13 | 14 | func testFetchRecipes() async throws { 15 | // Arrange step 16 | networkInterface.getSpy.stub(success: recipesArrayAsData) 17 | // `ThrowingPendableSpy` basically has the same interface as 18 | // `ThrowingSpy`. 19 | 20 | // Act step 21 | let recipes = try await subject.recipes() 22 | 23 | XCTAssertEqual(recipes, [...]) 24 | XCTAssertEqual( 25 | networkInterface.getSpy.calls, 26 | [URL(string: "https://example.com/recipes")!] 27 | ) 28 | } 29 | 30 | func testFetchRecipesRethrowsErrors() async throws { 31 | // ... 32 | } 33 | 34 | func testStoreRecipe() async throws { 35 | // Arrange step 36 | networkInterface.postSpy.stub(success: ()) // if we don't restub 37 | // `postSpy`, then we will end up blocking the test and throwing a 38 | // `PendableInProgressError`. 39 | 40 | // Act step 41 | try await subject.store(recipe: Recipe(...)) 42 | 43 | // Assert Step 44 | XCTAssertEqual(networkInterface.postSpy.calls.count, 1) 45 | XCTAssertEqual( 46 | networkInterface.postSpy.calls.last?.data, 47 | expectedRecipeData 48 | ) 49 | XCTAssertEqual( 50 | networkInterface.postSpy.calls.last?.url, 51 | URL(string: "https://example.com/recipes/store")! 52 | ) 53 | } 54 | 55 | func testStoreRecipeRethrowsErrors() async throws { 56 | // ... 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Tutorials/WritingFakes/Resources/code/RecipeService/RecipeServiceTests-02.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class RecipeServiceTests: XCTestCase { 4 | var networkInterface: FakeNetworkInterface! 5 | var subject: RecipeService! 6 | 7 | override func setUp() { 8 | super.setUp() 9 | 10 | networkInterface = FakeNetworkInterface() 11 | subject = RecipeService(networkInterface: networkInterface) 12 | } 13 | 14 | func testFetchRecipes() throws { 15 | // Arrange step 16 | networkInterface.getSpy.stub(success: recipesArrayAsData) // when using 17 | // `ThrowingSpy`, to set the Success stub, use the `stub(success:)` 18 | // overload. You could also use `stub(.success(...))` to do the same 19 | // thing, though. 20 | 21 | // Act step 22 | let recipes = try subject.recipes() 23 | 24 | XCTAssertEqual(recipes, [...]) 25 | XCTAssertEqual( 26 | networkInterface.getSpy.calls, 27 | [URL(string: "https://example.com/recipes")!] 28 | ) 29 | } 30 | 31 | func testFetchRecipesRethrowsErrors() { 32 | // Arrange step 33 | let expectedError = TestError() 34 | networkInterface.getSpy.stub(failure: expectedError) 35 | 36 | // Act step 37 | let result = Result { try subject.recipes() } 38 | 39 | // Assert step 40 | switch result { 41 | case .success: 42 | XCTFail("Expected `recipes` to throw an error, but succeeded.") 43 | case .failure(let failure): 44 | XCTAssertEqual(failure as TestError, expectedError) // because 45 | // we are specifically testing that the error from 46 | // `NetworkInterface.get` is rethrown, we should specifically 47 | // test that the thrown error is the same error we stubbed 48 | // networkInterface with. 49 | } 50 | } 51 | 52 | func testStoreRecipe() throws { 53 | // Act step 54 | try subject.store(recipe: Recipe(...)) 55 | 56 | // Assert Step 57 | XCTAssertEqual(networkInterface.postSpy.calls.count, 1) 58 | XCTAssertEqual( 59 | networkInterface.postSpy.calls.last?.data, 60 | expectedRecipeData 61 | ) 62 | XCTAssertEqual( 63 | networkInterface.postSpy.calls.last?.url, 64 | URL(string: "https://example.com/recipes/store")! 65 | ) 66 | } 67 | 68 | func testStoreRecipeRethrowsErrors() { 69 | // (You get the idea from `testFetchRecipesRethrowsErrors`) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/ci-swiftpm.yml: -------------------------------------------------------------------------------- 1 | name: CI (SwiftPM) 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "*" 9 | pull_request: 10 | 11 | jobs: 12 | filter: 13 | runs-on: ubuntu-latest 14 | outputs: 15 | should_skip: ${{ steps.skip_check.outputs.should_skip }} 16 | steps: 17 | - id: skip_check 18 | uses: fkirc/skip-duplicate-actions@v5.3.1 19 | with: 20 | paths: '[".github/workflows/ci-swiftpm.yml", "Sources/**", "Tests/**", "Package.*"]' 21 | do_not_skip: '["push", "workflow_dispatch", "schedule"]' 22 | 23 | swiftpm_darwin: 24 | name: SwiftPM, Darwin, Xcode ${{ matrix.xcode }} 25 | needs: filter 26 | runs-on: macos-14 27 | strategy: 28 | matrix: 29 | xcode: ["16.1"] 30 | fail-fast: false 31 | env: 32 | DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app" 33 | steps: 34 | - uses: actions/checkout@v6 35 | if: ${{ needs.filter.outputs.should_skip != 'true' }} 36 | - run: swift build 37 | if: ${{ needs.filter.outputs.should_skip != 'true' }} 38 | - run: swift test 39 | if: ${{ needs.filter.outputs.should_skip != 'true' }} 40 | 41 | swiftpm_darwin_15: 42 | name: SwiftPM, Darwin 15, Xcode ${{ matrix.xcode }} 43 | needs: filter 44 | runs-on: macos-15 45 | strategy: 46 | matrix: 47 | xcode: ["16.4"] 48 | fail-fast: false 49 | env: 50 | DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app" 51 | steps: 52 | - uses: actions/checkout@v6 53 | if: ${{ needs.filter.outputs.should_skip != 'true' }} 54 | - run: swift build 55 | if: ${{ needs.filter.outputs.should_skip != 'true' }} 56 | - run: swift test 57 | if: ${{ needs.filter.outputs.should_skip != 'true' }} 58 | 59 | swiftpm_linux: 60 | name: SwiftPM, Linux 61 | needs: filter 62 | runs-on: ubuntu-latest 63 | strategy: 64 | matrix: 65 | container: 66 | - swift:6.0 67 | - swift:6.1 68 | - swift:6.2 69 | fail-fast: false 70 | container: ${{ matrix.container }} 71 | steps: 72 | - uses: actions/checkout@v6 73 | if: ${{ needs.filter.outputs.should_skip != 'true' }} 74 | - run: swift build 75 | if: ${{ needs.filter.outputs.should_skip != 'true' }} 76 | - run: swift test 77 | if: ${{ needs.filter.outputs.should_skip != 'true' }} 78 | 79 | swiftpm_android: 80 | name: SwiftPM, Android 81 | needs: filter 82 | runs-on: ubuntu-latest 83 | steps: 84 | - uses: actions/checkout@v6 85 | - uses: skiptools/swift-android-action@v2 86 | 87 | -------------------------------------------------------------------------------- /Sources/Fakes/Spy/Spy+Testing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Spy+Testing.swift 3 | // swift-fakes 4 | // 5 | // Created by Rachel Brindle on 9/29/25. 6 | // 7 | 8 | extension Spy { 9 | /// Returns true if this spy has been called at least once. 10 | public var wasCalled: Bool { 11 | calls.isEmpty == false 12 | } 13 | 14 | /// Returns true if this spy has been called exactly as many times as specified. 15 | public func wasCalled(times: Int) -> Bool { 16 | calls.count == times 17 | } 18 | 19 | /// Returns true if this spy has not been called. 20 | public var wasNotCalled: Bool { 21 | calls.isEmpty 22 | } 23 | 24 | /// Returns whether this spy called at any time with the given value. 25 | public func wasCalled(with value: Arguments) -> Bool where Arguments: Equatable { 26 | calls.contains { call in 27 | call == value 28 | } 29 | } 30 | 31 | /// Returns whether this spy called with precisely these values, in this order. 32 | public func wasCalled(with values: [Arguments]) -> Bool where Arguments: Equatable { 33 | let currentCalls = calls 34 | guard currentCalls.count == values.count else { return false } 35 | 36 | for idx in 0.. Bool) -> Bool { 44 | calls.contains { call in 45 | matcher(call) 46 | } 47 | } 48 | 49 | /// Returns whether this spy was called with values that correspond to the order of closures given. 50 | /// 51 | /// For example, if this spy was called with `[1, 2]`, then `wasCalled(matching: [{ $0 == 1 }, { $0 == 2}])` would return true. 52 | public func wasCalled(matching matchers: [(Arguments) -> Bool]) -> Bool { 53 | let currentCalls = calls 54 | guard currentCalls.count == matchers.count else { return false } 55 | 56 | for idx in 0.. Bool where Arguments: Equatable { 64 | calls.last == value 65 | } 66 | 67 | /// Returns whether the most recent call to the spy matches the given closure. 68 | public func wasMostRecentlyCalled(matching matcher: (Arguments) -> Bool) -> Bool { 69 | guard let lastCall = calls.last else { return false } 70 | return matcher(lastCall) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Tutorials/WritingFakes/Resources/code/RecipeService/RecipeServiceTests-01.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class RecipeServiceTests: XCTestCase { 4 | var networkInterface: FakeNetworkInterface! 5 | var subject: RecipeService! 6 | 7 | override func setUp() { 8 | super.setUp() 9 | 10 | networkInterface = FakeNetworkInterface() 11 | subject = RecipeService(networkInterface: networkInterface) 12 | } 13 | 14 | func testFetchRecipes() throws { 15 | // Arrange step 16 | // First, we stub the networkInterface with some data that 17 | // converts to an array of recipes. Because we are using a real 18 | // JSONDecoder in RecipeService, we must provide data that can be 19 | // converted to [Recipe]. 20 | // Whatever you do, DO NOT start with an Array of Recipe, and convert 21 | // it to Data using `JSONEncoder`. That creates a tautology, and doesn't 22 | // actually check that your Recipe data can convert to actual Recipes. 23 | // If we update Recipe and forget to update the fixture, we want the 24 | // test to fail in order to let us know we either need to update the 25 | // fixture, or that we made a breaking change. 26 | networkInterface.getSpy.stub(recipesArrayAsData) 27 | 28 | // Act step 29 | let recipes = try subject.recipes() 30 | 31 | // Assert step 32 | XCTAssertEqual( 33 | recipes, 34 | [...] 35 | ) // verify that we retrieved and decoded the expected recipes. 36 | XCTAssertEqual( 37 | networkInterface.getSpy.calls, 38 | [URL(string: "https://example.com/recipes")!] 39 | ) // Verify that we actually made the call to networkInterface.get(from:) 40 | } 41 | 42 | func testStoreRecipe() throws { 43 | // Arrange step 44 | // Because NetworkInterface.post returns Void, we don't need 45 | // to stub anything. 46 | let recipe = Recipe(...) 47 | 48 | // Act step 49 | try subject.store(recipe: recipe) 50 | 51 | // Assert Step 52 | XCTAssertEqual( 53 | networkInterface.postSpy.calls.count, 54 | 1 55 | ) // Verify that only one call to post was made. 56 | XCTAssertEqual( 57 | networkInterface.postSpy.calls.last?.data, 58 | expectedRecipeData 59 | ) // Verify that the Recipe was converted to Data correctly. As with 60 | // `testFetchRecipes`, we want to have an actual fixture we can compare 61 | // with, and not just use JSONEncoder().encode(recipe). Again, if we 62 | // did so, that would create a tautology, and doesn't actually check 63 | // that we converted to Data correctly. 64 | XCTAssertEqual( 65 | networkInterface.postSpy.calls.last?.url, 66 | URL(string: "https://example.com/recipes/store")! 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Guides/NimbleIntegration.md: -------------------------------------------------------------------------------- 1 | # Nimble Integration 2 | 3 | Nimble Matchers to make asserting on ``Spy`` significantly nicer. 4 | 5 | ## Contents 6 | 7 | - The ``beCalled(_:)-82qlg`` without any arguments matches if the `Spy` has been 8 | called at least once. This is especially useful for verifying there are no 9 | interactions with the `Spy`, by using `expect(spy).toNot(beCalled())`. 10 | - The ``beCalled(_:times:)-6125c`` without any matcher arguments, matches if the 11 | `Spy` has been called exactly the amount of times specified in the `times` 12 | argument. For example, `expect(spy).to(beCalled(times: 3))` will pass if the 13 | `Spy` has been called with any arguments exactly 3 times. 14 | - The ``beCalled(_:)-82qlg`` matcher with any non-zero amount of matchers will 15 | match if at least one of the calls to the `Spy` matches all of the passed-in 16 | matchers. That is, `expect(spy).to(beCalled(equal(1), equal(2)))` will never 17 | pass, because no single call to the `Spy` can pass both `equal(1)` and 18 | `equal(2)`. 19 | - The ``beCalled(_:times:)-6125c`` matcher with a non-zero matcher arguments 20 | is effectively the same as `satisfyAllOf(beCalled(times:), beCalled(...))`. That 21 | is, it matches if the `Spy` has been called exactly `times` times, and at least 22 | one of the calls to the `Spy` matches all of the passed-in matchers. 23 | For example: 24 | 25 | ```swift 26 | let spy = Spy() 27 | spy(1) 28 | spy(2) 29 | 30 | expect(spy).to(beCalled(equal(1), times: 2)) 31 | ``` 32 | 33 | will match because the `Spy` has been called twice, and at least one of those 34 | calls is equal to 1. 35 | 36 | - The ``mostRecentlyBeCalled(_:)-9i9t9`` matcher will match if the last recorded 37 | call to the `Spy` matches all of the passed in matchers. Unlike the `beCalled` 38 | matchers, ``mostRecentlyBeCalled(_:)-9i9t9`` will unconditionally fail if you 39 | don't pass in any matchers. 40 | 41 | - For all of variants of `beCalled` and `mostRecentlyBeCalled`: If the 42 | `Arguments` to the `Spy` conforms to `Equatable`, you can directly pass in a 43 | value of the same type to `beCalled` or `mostRecentlyBeCalled`. For example, 44 | if you have a `Spy`, you can use `beCalled(123)` in place of 45 | `beCalled(equal(123))`. See ``beCalled(_:)-7sn1o``, 46 | ``beCalled(_:times:)-9320x``, and ``mostRecentlyBeCalled(_:)-91ves``. 47 | 48 | 49 | > Tip: If your `Spy` takes multiple `Arguments` - that is, the `Arguments` generic is a 50 | Tuple -, then you can make use of Nimble's built-in 51 | [`map` matcher](https://quick.github.io/Nimble/documentation/nimble/map(_:_:)-6ykjm) 52 | to easily verify a call to the matcher. For example: 53 | 54 | ```swift 55 | let spy = Spy<(arg1: Double, arg2: String), Void>() 56 | 57 | spy((1337.001, "hello world")) 58 | 59 | expect(spy).to(beCalled( 60 | map(\.arg1, beCloseTo(1337, within: 0.01)), 61 | map(\.arg2, beginWith("hello")) 62 | )) 63 | ``` 64 | 65 | > Note: If you want to check that a Spy is not ever called as part of code 66 | running on a background thread, use `expect(spy).toNever(beCalled())`. 67 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [quickorg@icloud.com](mailto:quickorg@icloud.com). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Sources/Fakes/PropertySpy/PropertySpy.swift: -------------------------------------------------------------------------------- 1 | /// An immutable property spy. 2 | @propertyWrapper public struct PropertySpy { 3 | public var wrappedValue: U { 4 | mapping(projectedValue()) 5 | } 6 | 7 | /// the ``Spy`` recording the getter calls. 8 | public let projectedValue: Spy 9 | 10 | public let mapping: @Sendable (T) -> U 11 | 12 | /// Creates an immutable PropertySpy stubbed with the given value, which lets you map from one type to another 13 | /// 14 | /// - parameter value: The initial value to be stubbed 15 | /// - parameter mapping: A closure to map from the initial value to the property's return type 16 | /// 17 | /// - Note: This initializer is particularly useful when the property is returning a protocol of some value, but you want to stub it with a particular instance of the protocol. 18 | public init(_ value: T, as mapping: @escaping @Sendable (T) -> U) { 19 | projectedValue = Spy(value) 20 | self.mapping = mapping 21 | } 22 | 23 | /// Creates an immutable PropertySpy stubbed with the given value 24 | /// 25 | /// - parameter value: The initial value to be stubbed 26 | public init(_ value: T) where T == U { 27 | projectedValue = Spy(value) 28 | self.mapping = { $0 } 29 | } 30 | } 31 | 32 | /// A mutable property spy. 33 | @propertyWrapper public struct SettablePropertySpy { 34 | public var wrappedValue: U { 35 | get { 36 | getMapping(projectedValue.getter()) 37 | } 38 | set { 39 | projectedValue.setter(newValue) 40 | projectedValue.getter.stub(setMapping(newValue)) 41 | } 42 | } 43 | 44 | public struct ProjectedValue { 45 | /// A ``Spy`` recording every time the property has been set, with whatever the new value is, prior to mapping 46 | public let setter: Spy 47 | /// A ``Spy`` recording every time the property has been called. It is re-stubbed whenever the property's setter is called. 48 | public let getter: Spy 49 | } 50 | 51 | /// The spies recording the setter and getter calls. 52 | public let projectedValue: ProjectedValue 53 | 54 | public let getMapping: @Sendable (T) -> U 55 | public let setMapping: @Sendable (U) -> T 56 | 57 | /// Creates a mutable PropertySpy stubbed with the given value, which lets you map from one type to another and back again 58 | /// 59 | /// - parameter value: The initial value to be stubbed 60 | /// - parameter getMapping: A closure to map from the initial value to the property's return type 61 | /// - parameter setMapping: A closure to map from the property's return type back to the initial value's type. 62 | /// 63 | /// - Note: This initializer is particularly useful when the property is returning a protocol of some value, but you want to stub it with a particular instance of the protocol. 64 | public init(_ value: T, getMapping: @escaping @Sendable (T) -> U, setMapping: @escaping @Sendable (U) -> T) { 65 | projectedValue = ProjectedValue(setter: Spy(), getter: Spy(value)) 66 | self.getMapping = getMapping 67 | self.setMapping = setMapping 68 | } 69 | 70 | /// Creatse a mutable PropertySpy stubbed with the given value 71 | /// 72 | /// - parameter value: The inital value to be stubbed 73 | public init(_ value: T) where T == U { 74 | projectedValue = ProjectedValue(setter: Spy(), getter: Spy(value)) 75 | self.getMapping = { $0 } 76 | self.setMapping = { $0 } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Fakes/Spy/Spy+Pendable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias PendableSpy< 4 | Arguments, 5 | Value 6 | > = Spy< 7 | Arguments, 8 | Pendable 9 | > 10 | 11 | extension Spy { 12 | /// Create a pendable Spy that is pre-stubbed to return return a a pending that will block for a bit before returning the fallback value. 13 | public convenience init(pendingFallback: Value) where Returning == Pendable { 14 | self.init(.pending(fallback: pendingFallback)) 15 | } 16 | 17 | /// Create a pendable Spy that is pre-stubbed to return return a a pending that will block for a bit before returning Void. 18 | public convenience init() where Returning == Pendable { 19 | self.init(.pending(fallback: ())) 20 | } 21 | 22 | /// Create a pendable Spy that is pre-stubbed to return a finished value. 23 | public convenience init(finished: Value) where Returning == Pendable { 24 | self.init(.finished(finished)) 25 | } 26 | } 27 | 28 | extension Spy { 29 | /// Resolve the pendable Spy's stub with Void 30 | public func resolveStub() where Returning == Pendable { 31 | self.resolveStub(with: ()) 32 | } 33 | } 34 | 35 | extension Spy { 36 | /// Update the pendable Spy's stub to be in a pending state. 37 | public func stub(pendingFallback: Value) where Returning == Pendable { 38 | self.stub(.pending(fallback: pendingFallback)) 39 | } 40 | 41 | /// Update the pendable Spy's stub to be in a pending state. 42 | public func stubPending() where Returning == Pendable { 43 | self.stub(.pending(fallback: ())) 44 | } 45 | 46 | /// Update the pendable Spy's stub to be in a pending state. 47 | public func stubPending() where Returning == Pendable> { 48 | // swiftlint:disable:previous syntactic_sugar 49 | self.stub(.pending(fallback: nil)) 50 | } 51 | 52 | /// Update the pendable Spy's stub to return the given value. 53 | /// 54 | /// - parameter finished: The value to return when `callAsFunction` is called. 55 | public func stub(finished: Value) where Returning == Pendable { 56 | self.stub(.finished(finished)) 57 | } 58 | 59 | /// Update the pendable Spy's stub to be in a pending state. 60 | public func stubFinished() where Returning == Pendable { 61 | self.stub(.finished(())) 62 | } 63 | } 64 | 65 | extension Spy { 66 | /// Records the arguments and handles the result according to ``Pendable/call(fallbackDelay:)``. 67 | /// 68 | /// - parameter arguments: The arguments to record. 69 | /// - parameter fallbackDelay: The amount of seconds to delay if the `Pendable` is pending before 70 | /// returning its fallback value. If the `Pendable` is finished, then this value is ignored. 71 | public func callAsFunction( 72 | _ arguments: Arguments, 73 | fallbackDelay: TimeInterval = PendableDefaults.delay 74 | ) async -> Value where Returning == Pendable { 75 | return await call(arguments).call(fallbackDelay: fallbackDelay) 76 | } 77 | 78 | /// Records that a call was made and handles the result according to ``Pendable/call(fallbackDelay:)``. 79 | /// 80 | /// - parameter fallbackDelay: The amount of seconds to delay if the `Pendable` is pending before 81 | /// returning its fallback value. If the `Pendable` is finished, then this value is ignored. 82 | public func callAsFunction( 83 | fallbackDelay: TimeInterval = PendableDefaults.delay 84 | ) async -> Value where Arguments == Void, Returning == Pendable { 85 | return await call(()).call(fallbackDelay: fallbackDelay) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/Fakes/Spy/Spy+Result.swift: -------------------------------------------------------------------------------- 1 | public typealias ThrowingSpy = Spy> 2 | 3 | extension Spy { 4 | /// Create a throwing Spy that is pre-stubbed with some Success values. 5 | public convenience init(success: Success, _ successes: Success...) where Returning == Result { 6 | self.init(Array(success, successes).map { .success($0) }) 7 | } 8 | 9 | /// Create a throwing Spy that is pre-stubbed with a Void Success value 10 | public convenience init() where Returning == Result { 11 | self.init(.success(())) 12 | } 13 | 14 | /// Create a throwing Spy that is pre-stubbed with some Failure error. 15 | public convenience init(failure: Failure) where Returning == Result { 16 | self.init(.failure(failure)) 17 | } 18 | 19 | public convenience init() where Returning == Result { 20 | self.init(.failure(EmptyError())) 21 | } 22 | 23 | public convenience init(_ closure: @escaping @Sendable (Arguments) throws(Failure) -> Success) where Returning == Result { 24 | self.init { args in 25 | do { 26 | return .success(try closure(args)) 27 | } catch let error as Failure { 28 | return .failure(error) 29 | } catch let error { 30 | fatalError("closure was typed to throw only errors of type \(Failure.self), got \(error). This shouldn't happen.") 31 | } 32 | } 33 | } 34 | } 35 | 36 | extension Spy { 37 | /// Update the throwing Spy's stub to be successful, with the given value. 38 | /// 39 | /// - parameter success: The success state to set the stub to, returned when `callAsFunction` is called. 40 | public func stub(success: Success, _ successes: Success...) where Returning == Result { 41 | self.stub(Array(success, successes).map { .success($0) }) 42 | } 43 | 44 | /// Update the throwing Spy's stub to be successful, with the given value. 45 | /// 46 | /// - parameter success: The success state to set the stub to, returned when `callAsFunction` is called. 47 | public func stub() where Returning == Result { 48 | self.stub(.success(())) 49 | } 50 | 51 | /// Update the throwing Spy's stub to throw the given error. 52 | /// 53 | /// - parameter failure: The error to throw when `callAsFunction` is called. 54 | public func stub(failure: Failure) where Returning == Result { 55 | self.stub(.failure(failure)) 56 | } 57 | 58 | public func stub(_ closure: @escaping @Sendable (Arguments) throws(Failure) -> Success) where Returning == Result { 59 | self.stub { args in 60 | do { 61 | return .success(try closure(args)) 62 | } catch let error as Failure { 63 | return .failure(error) 64 | } catch let error { 65 | fatalError("closure was typed to throw only errors of type \(Failure.self), got \(error). This shouldn't happen.") 66 | } 67 | } 68 | } 69 | } 70 | 71 | extension Spy { 72 | /// Records the arguments and returns the success (or throws an error), as defined by the current stub. 73 | public func callAsFunction(_ arguments: Arguments) throws -> Success where Returning == Result { 74 | return try call(arguments).get() 75 | } 76 | 77 | /// Records that a call was made and returns the success (or throws an error), as defined by the current stub. 78 | public func callAsFunction() throws -> Success where Arguments == Void, Returning == Result { 79 | return try call(()).get() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Tests/FakesTests/SpyTests+Nimble.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import XCTest 3 | import Fakes 4 | 5 | final class SpyNimbleMatchersTest: XCTestCase { 6 | func testBeCalledWithoutArguments() { 7 | let spy = Spy() 8 | 9 | // beCalled should match if spy has been called any number of times. 10 | expect(spy).toNot(beCalled()) 11 | 12 | spy(1) 13 | expect(spy).to(beCalled()) 14 | 15 | spy(2) 16 | expect(spy).to(beCalled()) 17 | } 18 | 19 | func testBeCalledWithArguments() { 20 | let spy = Spy() 21 | 22 | expect(spy).toNot(beCalled()) 23 | 24 | spy(3) 25 | expect(spy).toNot(beCalled(equal(2))) 26 | expect(spy).to(beCalled(equal(3))) 27 | 28 | // beCalled without a matcher argument is available when Arguments conforms to Equatable. 29 | expect(spy).toNot(beCalled(2)) 30 | expect(spy).to(beCalled(3)) 31 | } 32 | 33 | func testBeCalledWithMultipleArguments() { 34 | let spy = Spy() 35 | 36 | spy(3) 37 | expect(spy).toNot(beCalled( 38 | equal(2), 39 | beLessThan(4) 40 | )) 41 | expect(spy).to(beCalled( 42 | equal(3), 43 | beLessThan(4) 44 | )) 45 | } 46 | 47 | func testBeCalledWithTimes() { 48 | let spy = Spy() 49 | 50 | expect(spy).to(beCalled(times: 0)) 51 | expect(spy).toNot(beCalled(times: 2)) 52 | expect(spy).toNot(beCalled(times: 1)) 53 | 54 | spy(1) 55 | expect(spy).to(beCalled(times: 1)) 56 | expect(spy).toNot(beCalled(times: 2)) 57 | expect(spy).toNot(beCalled(times: 0)) 58 | 59 | spy(2) 60 | expect(spy).to(beCalled(times: 2)) 61 | expect(spy).toNot(beCalled(times: 0)) 62 | expect(spy).toNot(beCalled(times: 1)) 63 | } 64 | 65 | func testBeCalledWithArgumentsAndTimes() { 66 | let spy = Spy() 67 | 68 | spy(1) 69 | 70 | expect(spy).toNot(beCalled(equal(2), times: 1)) 71 | expect(spy).toNot(beCalled(3, times: 1)) 72 | expect(spy).to(beCalled(equal(1), times: 1)) 73 | expect(spy).to(beCalled(1, times: 1)) 74 | 75 | spy(3) 76 | expect(spy).toNot(beCalled(equal(2), times: 2)) 77 | expect(spy).to(beCalled(equal(3), times: 2)) 78 | 79 | expect(spy).toNot(beCalled(2, times: 2)) 80 | expect(spy).to(beCalled(3, times: 2)) 81 | } 82 | 83 | func testBeCalledWithMultipleArgumentsAndTimes() { 84 | let spy = Spy() 85 | 86 | spy(1) 87 | 88 | expect(spy).toNot(beCalled(equal(2), beLessThan(3), times: 1)) 89 | expect(spy).to(beCalled(equal(1), beLessThan(3), times: 1)) 90 | 91 | spy(3) 92 | expect(spy).toNot(beCalled(equal(2), beLessThan(4), times: 2)) 93 | expect(spy).to(beCalled(equal(3), beLessThan(4), times: 2)) 94 | } 95 | 96 | func testMostRecentlyBeCalled() { 97 | let spy = Spy() 98 | 99 | spy(1) 100 | expect(spy).to(mostRecentlyBeCalled(equal(1))) 101 | expect(spy).toNot(mostRecentlyBeCalled(equal(2))) 102 | expect(spy).to(mostRecentlyBeCalled(1)) 103 | expect(spy).toNot(mostRecentlyBeCalled(2)) 104 | 105 | spy(2) 106 | expect(spy).toNot(mostRecentlyBeCalled(equal(1))) 107 | expect(spy).to(mostRecentlyBeCalled(equal(2))) 108 | 109 | expect(spy).toNot(mostRecentlyBeCalled(1)) 110 | expect(spy).to(mostRecentlyBeCalled(2)) 111 | } 112 | 113 | func testMostRecentlyBeCalledWithMultipleArguments() { 114 | let spy = Spy() 115 | 116 | spy(1) 117 | expect(spy).to(mostRecentlyBeCalled( 118 | equal(1), 119 | beLessThan(3) 120 | )) 121 | expect(spy).toNot(mostRecentlyBeCalled( 122 | equal(2), 123 | beLessThan(3) 124 | )) 125 | 126 | spy(2) 127 | expect(spy).toNot(mostRecentlyBeCalled( 128 | equal(1), 129 | beLessThan(3) 130 | )) 131 | expect(spy).to(mostRecentlyBeCalled( 132 | equal(2), 133 | beLessThan(3) 134 | )) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](http://doctoc.herokuapp.com/)* 4 | 5 | - [Welcome to Swift Fakes!](#welcome-to-swift-fakes) 6 | - [Reporting Bugs](#reporting-bugs) 7 | - [Building the Project](#building-the-project) 8 | - [Pull Requests](#pull-requests) 9 | - [Style Conventions](#style-conventions) 10 | - [Core Members](#core-members) 11 | - [Creating a Release](#creating-a-release) 12 | 13 | 14 | 15 | # Welcome to Swift Fakes! 16 | 17 | We're building standardized test doubles for Swift. 18 | 19 | Swift Fakes should be easy to use and easy to maintain. Let's keep things 20 | simple and well-tested. 21 | 22 | ## Reporting Bugs 23 | 24 | Nothing is off-limits. If you're having a problem, we want to hear about 25 | it. 26 | 27 | - See a crash? File an issue. 28 | - Code isn't compiling, but you don't know why? Sounds like you should 29 | submit a new issue, friend. 30 | - Went to the kitchen, only to forget why you went in the first place? 31 | Better submit an issue. 32 | 33 | Be sure to include in your issue: 34 | 35 | - Your Xcode version (eg - Xcode 15.3.0 15E204A) 36 | - Your version of Quick / Nimble / Swift Fakes (eg - v0.0.0 or git sha `7d0b8c21357839a8c5228863b77faecf709254a9`) 37 | - What are the steps to reproduce this issue? 38 | - What platform are you using? (eg - OS X, iOS, watchOS, tvOS, visionOS) 39 | - If the problem is on a UI Testing Bundle, Unit Testing Bundle, or some other target configuration 40 | - Are you using Swift PM or some other package manager? 41 | 42 | ## Building the Project 43 | 44 | - Open the `Package.swift` in xcode, or run `swift build`. 45 | 46 | ## Pull Requests 47 | 48 | - Nothing is trivial. Submit pull requests for anything: typos, 49 | whitespace, you name it. 50 | - Not all pull requests will be merged, but all will be acknowledged. If 51 | no one has provided feedback on your request, ping one of the owners 52 | by name. 53 | - Make sure your pull request includes any necessary updates to the 54 | README or other documentation. 55 | - Be sure the unit tests pass before submitting your pull request. You can run all 56 | the unit tests using `swift test`. 57 | - The `main` branch will always support the stable Xcode version. Other 58 | branches will point to their corresponding versions they support. 59 | 60 | ### Style Conventions 61 | 62 | - Indent using 4 spaces. 63 | - Keep lines 100 characters or shorter. Break long statements into 64 | shorter ones over multiple lines. 65 | 66 | ## Core Members 67 | 68 | If a few of your pull requests have been merged, and you'd like a 69 | controlling stake in the project, file an issue asking for write access 70 | to the repository. 71 | 72 | Your conduct as a core member is your own responsibility, but here are 73 | some "ground rules": 74 | 75 | - Feel free to push whatever you want to main, and (if you have 76 | ownership permissions) to create any repositories you'd like. 77 | 78 | Ideally, however, all changes should be submitted as GitHub pull 79 | requests. No one should merge their own pull request, unless no 80 | other core members respond for at least a few days. 81 | 82 | Pull requests should be issued from personal forks. The Quick repo 83 | should be reserved for long-running feature branches. 84 | 85 | If you'd like to create a new repository, it'd be nice if you created 86 | a GitHub issue and gathered some feedback first. 87 | 88 | - It'd be awesome if you could review, provide feedback on, and close 89 | issues or pull requests submitted to the project. Please provide kind, 90 | constructive feedback. Please don't be sarcastic or snarky. 91 | 92 | Read [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for more in depth guidelines. 93 | 94 | ## Creating a Release 95 | 96 | The process is relatively straight forward, but here's is a useful checklist for tagging: 97 | 98 | - Run the release script: `./script/release A.B.C` 99 | - The script create a new [GitHub release](https://github.com/Quick/swift-fakes/releases) with autogenerated release notes. 100 | - Edit the release notes, and add highlights. Be especially sure to call out any breaking changes. 101 | - Announce! 102 | -------------------------------------------------------------------------------- /Tests/FakesTests/DynamicResultTests.swift: -------------------------------------------------------------------------------- 1 | import Fakes 2 | import Nimble 3 | import XCTest 4 | 5 | final class DynamicResultTests: XCTestCase { 6 | func testSingleStaticValue() { 7 | let subject = DynamicResult(1) 8 | 9 | expect(subject.call(1)).to(equal(1)) 10 | expect(subject.call(1)).to(equal(1)) 11 | expect(subject.call(1)).to(equal(1)) 12 | } 13 | 14 | func testMultipleStaticValues() { 15 | let subject = DynamicResult(1, 2, 3) 16 | 17 | expect(subject.call()).to(equal(1)) 18 | expect(subject.call()).to(equal(2)) 19 | expect(subject.call()).to(equal(3)) 20 | // After the last call, we continue to return the last stub in the list 21 | expect(subject.call()).to(equal(3)) 22 | expect(subject.call()).to(equal(3)) 23 | } 24 | 25 | func testClosure() { 26 | let subject = DynamicResult({ $0 + 1 }) 27 | 28 | expect(subject.call(1)).to(equal(2)) 29 | expect(subject.call(2)).to(equal(3)) 30 | expect(subject.call(3)).to(equal(4)) 31 | } 32 | 33 | func testStubs() { 34 | let subject = DynamicResult( 35 | .value(1), 36 | .closure({ $0 * 3}), 37 | .value(4) 38 | ) 39 | 40 | expect(subject.call(1)).to(equal(1)) 41 | expect(subject.call(2)).to(equal(6)) 42 | expect(subject.call(3)).to(equal(4)) 43 | expect(subject.call(3)).to(equal(4)) 44 | } 45 | 46 | // MARK: - Replacement Tests 47 | 48 | func testReplacementStaticValue() { 49 | let subject = DynamicResult(1) 50 | 51 | subject.replace(2, 3, 4, 6) 52 | 53 | expect(subject.call()).to(equal(2)) 54 | expect(subject.call()).to(equal(3)) 55 | expect(subject.call()).to(equal(4)) 56 | expect(subject.call()).to(equal(6)) 57 | // After the last call, we continue to return the last stub in the list 58 | expect(subject.call()).to(equal(6)) 59 | } 60 | 61 | func testReplacementClosure() { 62 | let subject = DynamicResult(1) 63 | 64 | subject.replace({ $0 + 4}) 65 | 66 | expect(subject.call(1)).to(equal(5)) 67 | expect(subject.call(2)).to(equal(6)) 68 | expect(subject.call(3)).to(equal(7)) 69 | } 70 | 71 | func testReplacementStubs() { 72 | let subject = DynamicResult(1) 73 | 74 | subject.replace( 75 | .value(2), 76 | .closure({ $0 * 3}), 77 | .value(4) 78 | ) 79 | 80 | expect(subject.call(1)).to(equal(2)) 81 | expect(subject.call(2)).to(equal(6)) 82 | expect(subject.call(3)).to(equal(4)) 83 | expect(subject.call(3)).to(equal(4)) 84 | } 85 | 86 | // MARK: - Appending Tests 87 | 88 | func testAppendingStaticValue() { 89 | let subject = DynamicResult(1, 2, 3) 90 | 91 | subject.append(4, 5, 6) 92 | 93 | expect(subject.call()).to(equal(1)) 94 | expect(subject.call()).to(equal(2)) 95 | expect(subject.call()).to(equal(3)) 96 | expect(subject.call()).to(equal(4)) 97 | expect(subject.call()).to(equal(5)) 98 | expect(subject.call()).to(equal(6)) 99 | // After the last call, we continue to return the last stub in the list 100 | expect(subject.call()).to(equal(6)) 101 | } 102 | 103 | func testAppendingClosure() { 104 | let subject = DynamicResult(1, 2, 3) 105 | 106 | subject.append({ $0 + 10}) 107 | 108 | expect(subject.call(1)).to(equal(1)) 109 | expect(subject.call(2)).to(equal(2)) 110 | expect(subject.call(3)).to(equal(3)) 111 | expect(subject.call(4)).to(equal(14)) 112 | expect(subject.call(5)).to(equal(15)) 113 | } 114 | 115 | func testAppendingStubs() { 116 | let subject = DynamicResult(1, 2, 3) 117 | 118 | subject.append( 119 | .value(4), 120 | .closure({ $0 * 3}), 121 | .value(6) 122 | ) 123 | 124 | expect(subject.call(1)).to(equal(1)) 125 | expect(subject.call(2)).to(equal(2)) 126 | expect(subject.call(3)).to(equal(3)) 127 | expect(subject.call(4)).to(equal(4)) 128 | expect(subject.call(5)).to(equal(15)) 129 | expect(subject.call(6)).to(equal(6)) 130 | expect(subject.call(7)).to(equal(6)) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Tests/FakesTests/SpyTests+TestHelpers.swift: -------------------------------------------------------------------------------- 1 | import Fakes 2 | import Testing 3 | 4 | struct SpyTestHelpersTest { 5 | @Test func wasCalledWithoutArguments() { 6 | let spy = Spy() 7 | 8 | // beCalled should match if spy has been called any number of times. 9 | #expect(spy.wasCalled == false) 10 | #expect(spy.wasNotCalled) 11 | 12 | spy(1) 13 | #expect(spy.wasCalled) 14 | #expect(spy.wasNotCalled == false) 15 | 16 | spy(2) 17 | #expect(spy.wasCalled) 18 | #expect(spy.wasNotCalled == false) 19 | } 20 | 21 | @Test func wasCalledWithArguments() { 22 | let spy = Spy() 23 | let otherSpy = Spy() 24 | 25 | spy(3) 26 | otherSpy(ANonEquatable(value: 3)) 27 | #expect(spy.wasCalled(with: 3)) 28 | #expect(spy.wasCalled(with: 2) == false) 29 | 30 | #expect(spy.wasCalled(matching: { $0 == 3 })) 31 | #expect(spy.wasCalled(matching: { $0 == 2 }) == false) 32 | 33 | #expect(otherSpy.wasCalled(matching: { $0.value == 3 })) 34 | #expect(otherSpy.wasCalled(matching: { $0.value == 2 }) == false) 35 | 36 | spy(4) 37 | otherSpy(ANonEquatable(value: 4)) 38 | #expect(spy.wasCalled(with: 3)) 39 | #expect(spy.wasCalled(matching: { $0 == 3 })) 40 | 41 | #expect(spy.wasCalled(with: 4)) 42 | #expect(spy.wasCalled(matching: { $0 == 4 })) 43 | 44 | #expect(otherSpy.wasCalled(matching: { $0.value == 4 })) 45 | #expect(otherSpy.wasCalled(matching: { $0.value == 3 })) 46 | } 47 | 48 | @Test func wasCalledWithTimes() { 49 | let spy = Spy() 50 | 51 | #expect(spy.wasCalled(times: 0)) 52 | #expect(spy.wasCalled(times: 1) == false) 53 | #expect(spy.wasCalled(times: 2) == false) 54 | 55 | spy(1) 56 | #expect(spy.wasCalled(times: 0) == false) 57 | #expect(spy.wasCalled(times: 1)) 58 | #expect(spy.wasCalled(times: 2) == false) 59 | 60 | spy(2) 61 | #expect(spy.wasCalled(times: 0) == false) 62 | #expect(spy.wasCalled(times: 1) == false) 63 | #expect(spy.wasCalled(times: 2)) 64 | } 65 | 66 | @Test func wasCalledWithMultipleArgumentsAndTimes() { 67 | let spy = Spy() 68 | let otherSpy = Spy() 69 | 70 | spy(1) 71 | otherSpy(ANonEquatable(value: 1)) 72 | spy(3) 73 | otherSpy(ANonEquatable(value: 3)) 74 | 75 | #expect(spy.wasCalled(with: [1, 3])) 76 | #expect(spy.wasCalled(with: [3, 1]) == false) // order matters 77 | 78 | #expect(spy.wasCalled( 79 | matching: [ 80 | { $0 == 1 }, 81 | { $0 == 3 } 82 | ] 83 | )) 84 | #expect(spy.wasCalled( 85 | matching: [ 86 | { $0 == 3 }, 87 | { $0 == 1 } 88 | ] 89 | ) == false) 90 | 91 | #expect(otherSpy.wasCalled( 92 | matching: [ 93 | { $0.value == 1 }, 94 | { $0.value == 3 } 95 | ] 96 | )) 97 | #expect(otherSpy.wasCalled( 98 | matching: [ 99 | { $0.value == 3 }, 100 | { $0.value == 1 } 101 | ] 102 | ) == false) 103 | } 104 | 105 | @Test func testMostRecentlyBeCalled() { 106 | let spy = Spy() 107 | let otherSpy = Spy() 108 | 109 | spy(1) 110 | otherSpy(ANonEquatable(value: 1)) 111 | #expect(spy.wasMostRecentlyCalled(with: 1)) 112 | #expect(spy.wasMostRecentlyCalled(with: 2) == false) 113 | 114 | #expect(spy.wasMostRecentlyCalled(matching: { $0 == 1})) 115 | #expect(spy.wasMostRecentlyCalled(matching: { $0 == 2}) == false) 116 | 117 | #expect(otherSpy.wasMostRecentlyCalled(matching: { $0.value == 1})) 118 | #expect(otherSpy.wasMostRecentlyCalled(matching: { $0.value == 2}) == false) 119 | 120 | spy(2) 121 | otherSpy(ANonEquatable(value: 2)) 122 | #expect(spy.wasMostRecentlyCalled(with: 1) == false) 123 | #expect(spy.wasMostRecentlyCalled(with: 2)) 124 | 125 | #expect(spy.wasMostRecentlyCalled(matching: { $0 == 1}) == false) 126 | #expect(spy.wasMostRecentlyCalled(matching: { $0 == 2})) 127 | 128 | #expect(otherSpy.wasMostRecentlyCalled(matching: { $0.value == 1}) == false) 129 | #expect(otherSpy.wasMostRecentlyCalled(matching: { $0.value == 2})) 130 | } 131 | } 132 | 133 | struct ANonEquatable { 134 | let value: Int 135 | } 136 | -------------------------------------------------------------------------------- /Tests/FakesTests/PropertySpyTests.swift: -------------------------------------------------------------------------------- 1 | import Fakes 2 | import Nimble 3 | import XCTest 4 | 5 | final class SettablePropertySpyTests: XCTestCase { 6 | func testGettingPropertyWhenTypesMatch() { 7 | struct AnObject { 8 | @SettablePropertySpy(1) 9 | var value: Int 10 | } 11 | 12 | let object = AnObject() 13 | 14 | expect(object.value).to(equal(1)) 15 | // because we called it, we should expect for the getter spy to be called 16 | expect(object.$value.getter).to(beCalled()) 17 | 18 | // We never interacted with the setter, so it shouldn't have been called. 19 | expect(object.$value.setter).toNot(beCalled()) 20 | } 21 | 22 | func testSettingPropertyWhenTypesMatch() { 23 | struct AnObject { 24 | @SettablePropertySpy(1) 25 | var value: Int 26 | } 27 | 28 | var object = AnObject() 29 | object.value = 3 30 | 31 | expect(object.$value.getter).toNot(beCalled()) 32 | expect(object.$value.setter).to(beCalled(3)) 33 | 34 | // the returned value should now be updated with the new value 35 | expect(object.value).to(equal(3)) 36 | 37 | // and because we called the getter, the getter spy should be called. 38 | expect(object.$value.getter).to(beCalled()) 39 | } 40 | 41 | func testGettingPropertyProtocolInheritence() { 42 | struct ImplementedProtocol: SomeProtocol { 43 | var value: Int = 1 44 | } 45 | 46 | struct AnObject { 47 | @SettablePropertySpy(ImplementedProtocol(value: 2)) 48 | var value: SomeProtocol 49 | } 50 | 51 | let object = AnObject() 52 | 53 | expect(object.value).to(beAKindOf(ImplementedProtocol.self)) 54 | // because we called it, we should expect for the getter spy to be called 55 | expect(object.$value.getter).to(beCalled()) 56 | 57 | // We never interacted with the setter, so it shouldn't have been called. 58 | expect(object.$value.setter).toNot(beCalled()) 59 | } 60 | 61 | func testSettingPropertyProtocolInheritence() { 62 | struct ImplementedProtocol: SomeProtocol, Equatable { 63 | var value: Int = 1 64 | } 65 | 66 | struct AnObject { 67 | @SettablePropertySpy(ImplementedProtocol()) 68 | var value: SomeProtocol 69 | } 70 | 71 | var object = AnObject() 72 | object.value = ImplementedProtocol(value: 2) 73 | 74 | expect(object.$value.getter).toNot(beCalled()) 75 | expect(object.$value.setter).to(beCalled(satisfyAllOf( 76 | beAKindOf(ImplementedProtocol.self), 77 | map(\.value, equal(2)) 78 | ))) 79 | 80 | // the returned value should now be updated with the new value 81 | expect(object.value).to(satisfyAllOf( 82 | beAKindOf(ImplementedProtocol.self), 83 | map(\.value, equal(2)) 84 | )) 85 | // and because we called the getter, the getter spy should be called. 86 | expect(object.$value.getter).to(beCalled(times: 1)) 87 | } 88 | } 89 | 90 | final class PropertySpyTests: XCTestCase { 91 | func testGettingPropertyWhenTypesMatch() { 92 | struct AnObject { 93 | @PropertySpy(1) 94 | var value: Int 95 | } 96 | 97 | let object = AnObject() 98 | 99 | expect(object.value).to(equal(1)) 100 | // because we called it, we should expect for the getter spy to be called 101 | expect(object.$value).to(beCalled()) 102 | } 103 | 104 | func testGettingPropertyProtocolInheritence() { 105 | struct ImplementedProtocol: SomeProtocol { 106 | var value: Int = 1 107 | } 108 | 109 | struct ObjectUsingProtocol { 110 | @PropertySpy(ImplementedProtocol(value: 2)) 111 | var value: SomeProtocol 112 | } 113 | 114 | struct ObjectUsingDirectInstance { 115 | @PropertySpy(ImplementedProtocol(value: 2), as: { $0 }) 116 | var value: SomeProtocol 117 | } 118 | 119 | let object = ObjectUsingProtocol() 120 | 121 | expect(object.value).to(beAnInstanceOf(ImplementedProtocol.self)) 122 | // because we called it, we should expect for the getter spy to be called 123 | expect(object.$value).to(beCalled()) 124 | expect(object.$value).to(beAnInstanceOf(Spy.self)) 125 | 126 | let otherObject = ObjectUsingDirectInstance() 127 | 128 | expect(otherObject.$value).to(beAnInstanceOf(Spy.self)) 129 | } 130 | } 131 | 132 | protocol SomeProtocol { 133 | var value: Int { get } 134 | } 135 | -------------------------------------------------------------------------------- /script/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | REMOTE_BRANCH=main 3 | GH=${GH:-"gh"} 4 | 5 | function help { 6 | echo "Usage: release VERSION [-f]" 7 | echo 8 | echo "VERSION should be the version to release, should not include the 'v' prefix" 9 | echo 10 | echo "FLAGS" 11 | echo " -f Forces override of tag" 12 | echo 13 | echo " Example: ./release 1.0.0-rc.2" 14 | echo 15 | exit 2 16 | } 17 | 18 | function die { 19 | echo "[ERROR] $@" 20 | echo 21 | exit 1 22 | } 23 | 24 | if [ $# -lt 1 ]; then 25 | help 26 | fi 27 | 28 | VERSION=$1 29 | FORCE_TAG=$2 30 | 31 | VERSION_TAG="v$VERSION" 32 | 33 | if [ -z "`which $GH`" ]; then 34 | die "gh (github CLI) is required to produce a release. Install with brew using 'brew install gh'. Aborting." 35 | fi 36 | 37 | echo " > Verifying you are authenticated with the github CLI" 38 | $GH auth status > /dev/null || die "You are not authenticated with the github CLI. Please authenticate using '$GH auth login'." 39 | echo " > Logged in with github CLI" 40 | 41 | echo " > Is this a reasonable tag?" 42 | 43 | echo $VERSION_TAG | grep -q "^vv" 44 | if [ $? -eq 0 ]; then 45 | die "This tag ($VERSION) is an incorrect format. You should remove the 'v' prefix." 46 | fi 47 | 48 | echo $VERSION_TAG | grep -q -E "^v\d+\.\d+\.\d+(-\w+(\.\d)?)?\$" 49 | if [ $? -ne 0 ]; then 50 | die "This tag ($VERSION) is an incorrect format. It should be in 'v{MAJOR}.{MINOR}.{PATCH}(-{PRERELEASE_NAME}.{PRERELEASE_VERSION})' form." 51 | fi 52 | 53 | echo " > Is this version ($VERSION) unique?" 54 | git describe --exact-match "$VERSION_TAG" > /dev/null 2>&1 55 | if [ $? -eq 0 ]; then 56 | if [ -z "$FORCE_TAG" ]; then 57 | die "This tag ($VERSION) already exists. Aborting. Append '-f' to override" 58 | else 59 | echo " > NO, but force was specified." 60 | fi 61 | else 62 | echo " > Yes, tag is unique" 63 | fi 64 | 65 | git config --get user.signingkey > /dev/null || { 66 | echo "[ERROR] No PGP found to sign tag. Aborting." 67 | echo 68 | echo " Creating a release requires signing the tag for security purposes. This allows users to verify the git cloned tree is from a trusted source." 69 | echo " From a security perspective, it is not considered safe to trust the commits (including Author & Signed-off fields). It is easy for any" 70 | echo " intermediate between you and the end-users to modify the git repository." 71 | echo 72 | echo " While not all users may choose to verify the PGP key for tagged releases. It is a good measure to ensure 'this is an official release'" 73 | echo " from the official maintainers." 74 | echo 75 | echo " If you're creating your PGP key for the first time, use RSA with at least 4096 bits." 76 | echo 77 | echo "Related resources:" 78 | echo " - Configuring your system for PGP: https://git-scm.com/book/tr/v2/Git-Tools-Signing-Your-Work" 79 | echo " - Why: http://programmers.stackexchange.com/questions/212192/what-are-the-advantages-and-disadvantages-of-cryptographically-signing-commits-a" 80 | echo 81 | exit 2 82 | } 83 | echo " > Found PGP key for git" 84 | 85 | echo "-> Ensuring no differences to origin/$REMOTE_BRANCH" 86 | git fetch origin || die "Failed to fetch origin" 87 | git diff --quiet HEAD "origin/$REMOTE_BRANCH" || die "HEAD is not aligned to origin/$REMOTE_BRANCH. Cannot update version safely" 88 | 89 | RELEASE_NOTES="Version ${VERSION}. Open https://github.com/Quick/swift-fakes/releases/tag/$VERSION_TAG for full release notes." 90 | 91 | if [ -z "$FORCE_TAG" ]; then 92 | echo "-> Tagging version" 93 | git tag -s "$VERSION_TAG" -m "$RELEASE_NOTES" || die "Failed to tag version" 94 | echo "-> Pushing tag to origin" 95 | git push origin "$VERSION_TAG" || die "Failed to push tag '$VERSION_TAG' to origin" 96 | else 97 | echo "-> Tagging version (force)" 98 | git tag -s -f "$VERSION_TAG" -m "$RELEASE_NOTES" || die "Failed to tag version" 99 | echo "-> Pushing tag to origin (force)" 100 | git push origin "$VERSION_TAG" -f || die "Failed to push tag '$VERSION_TAG' to origin" 101 | fi 102 | 103 | # Check version tag to determine whether to mark the release as a prerelease version or not. 104 | echo $VERSION_TAG | grep -q -E "^v\d+\.\d+\.\d+\$" 105 | if [ $? -eq 0 ]; then 106 | PRERELEASE_FLAGS="" 107 | else 108 | PRERELEASE_FLAGS="-p" 109 | fi 110 | 111 | echo "-> Creating a github release using auto-generated notes." 112 | 113 | $GH release create -R Quick/swift-fakes $VERSION_TAG --generate-notes $PRERELEASE_FLAGS 114 | 115 | echo 116 | echo "================ Finalizing the Release ================" 117 | echo 118 | echo " - Opening GitHub to allow for any edits to the release notes." 119 | echo " - You should add a Highlights section at the top to call out any notable changes or fixes." 120 | echo " - In particular, any breaking changes should be listed under Highlights." 121 | echo " - Carthage archive frameworks will be automatically uploaded after the release is published." 122 | echo " - Announce!" 123 | 124 | open "https://github.com/Quick/swift-fakes/releases/tag/$VERSION_TAG" 125 | -------------------------------------------------------------------------------- /Sources/Fakes/Spy/Spy.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A Spy is a test double for recording calls to methods, and returning stubbed results. 4 | /// 5 | /// Spies should be used to verify that Fakes are called correctly, and to provide pre-stubbed values 6 | /// that the Fake returns to the caller. 7 | public final class Spy { 8 | public typealias Stub = DynamicResult.Stub 9 | 10 | private let lock = NSRecursiveLock() 11 | 12 | private var _calls: [Arguments] = [] 13 | public var calls: [Arguments] { 14 | lock.lock() 15 | defer { lock.unlock() } 16 | return _calls 17 | } 18 | 19 | private var _stub: DynamicResult 20 | 21 | // MARK: - Initializers 22 | /// Create a Spy with the given stubbed values. 23 | public init(_ value: Returning, _ values: Returning...) { 24 | _stub = DynamicResult(Array(value, values)) 25 | } 26 | 27 | internal init(_ values: [Returning]) { 28 | _stub = DynamicResult(values) 29 | } 30 | 31 | public init(_ closure: @escaping @Sendable (Arguments) -> Returning) { 32 | _stub = DynamicResult(closure) 33 | } 34 | 35 | public init(_ stub: Stub, _ stubs: Stub...) { 36 | _stub = DynamicResult(Array(stub, stubs)) 37 | } 38 | 39 | internal init(_ stubs: [Stub]) { 40 | _stub = DynamicResult(stubs) 41 | } 42 | 43 | /// Create a Spy that returns Void 44 | public convenience init() where Returning == Void { 45 | self.init(()) 46 | } 47 | 48 | /// Create a Spy that returns nil 49 | public convenience init() where Returning == Optional { 50 | // swiftlint:disable:previous syntactic_sugar 51 | self.init(nil) 52 | } 53 | 54 | /// Clear out existing call records. 55 | /// 56 | /// This removes all previously recorded calls from the spy. It does not otherwise 57 | /// mutate the spy. 58 | public func clearCalls() { 59 | lock.lock() 60 | defer { lock.unlock () } 61 | _calls = [] 62 | } 63 | 64 | // MARK: Stubbing 65 | /// Replaces the Spy's stubs with the given values. 66 | /// 67 | /// - parameter value: The first value to return when `callAsFunction()` is called. 68 | /// - parameter values: The list of other values (in order) to return when `callAsFunction()` is called. 69 | /// 70 | /// - Note: This resolves any pending Pendables during replacement. 71 | public func stub(_ value: Returning, _ values: Returning...) { 72 | stub(Array(value, values)) 73 | } 74 | 75 | internal func stub(_ values: [Returning]) { 76 | lock.lock() 77 | defer { lock.unlock () } 78 | _stub.replace(values) 79 | } 80 | 81 | /// Replaces the Spy's stubs with the given closure. 82 | /// 83 | /// - parameter closure: The closure to call with the arguments when `callAsFunction()` is called. 84 | /// 85 | /// - Note: This resolves any pending Pendables during replacement. 86 | public func stub(_ closure: @escaping @Sendable (Arguments) -> Returning) { 87 | lock.lock() 88 | defer { lock.unlock () } 89 | _stub.replace(closure) 90 | } 91 | 92 | /// Replace the Spy's stubs with the new list of stubs 93 | /// 94 | /// - parameter stub: The first stub to call when `callAsFunction()` is called. 95 | /// - parameter stubs: The list of other stubs (in order) to return when `callAsFunction()` is called. 96 | /// 97 | /// - Note: This resolves any pending Pendables during replacement. 98 | public func replace( 99 | _ stub: Stub, 100 | _ stubs: Stub... 101 | ) { 102 | lock.lock() 103 | defer { lock.unlock () } 104 | _stub.replace(Array(stub, stubs)) 105 | } 106 | 107 | /// Append the values to the Spy's stubs 108 | /// 109 | /// - parameter value: The first value to append to the list of stubs 110 | /// - parameter values: The remaining values to append to the list of stubs 111 | public func append(_ value: Returning, _ values: Returning...) { 112 | lock.lock() 113 | defer { lock.unlock () } 114 | _stub.append(Array(value, values)) 115 | } 116 | 117 | /// Append the closure to the list of Spy's stubs 118 | /// 119 | /// - parameter closure: The new closure to call at the end of the list of stubs 120 | public func append(_ closure: @escaping @Sendable (Arguments) -> Returning) { 121 | lock.lock() 122 | defer { lock.unlock () } 123 | _stub.append(closure) 124 | } 125 | 126 | /// Append the stubs to the list of stubs. 127 | /// 128 | /// - parameter value: The first stub to append to the list of stubs 129 | /// - parameter values: The remaining stubs to append to the list of stubs 130 | public func append( 131 | _ stub: Stub, 132 | _ stubs: Stub... 133 | ) { 134 | lock.lock() 135 | defer { lock.unlock () } 136 | _stub.append(Array(stub, stubs)) 137 | } 138 | 139 | internal func call(_ arguments: Arguments) -> Returning { 140 | lock.lock() 141 | defer { lock.unlock() } 142 | 143 | _calls.append(arguments) 144 | 145 | return _stub.call(arguments) 146 | } 147 | } 148 | 149 | extension Spy { 150 | // MARK: - Calling 151 | /// Records the arguments and returns the value stubbed in the initializer, or using one of the `stub()` methods. 152 | public func callAsFunction(_ arguments: Arguments) -> Returning { 153 | return call(arguments) 154 | } 155 | 156 | /// Records that a call was made and returns the value stubbed in the initializer, or using one of the `stub()` methods. 157 | public func callAsFunction() -> Returning where Arguments == Void { 158 | return call(()) 159 | } 160 | } 161 | 162 | extension Spy { 163 | public func resolveStub(with value: Value) where Returning == Pendable { 164 | lock.lock() 165 | defer { lock.unlock() } 166 | _stub.stubHistory.forEach { 167 | $0.resolve(with: value) 168 | } 169 | } 170 | } 171 | 172 | extension Spy: @unchecked Sendable where Arguments: Sendable, Returning: Sendable {} 173 | -------------------------------------------------------------------------------- /Sources/Fakes/DynamicResult.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A DynamicResult is specifically for mocking out when multiple results for a call can happen. 4 | /// 5 | /// DynamicResult is intended to be an implementation detail of ``Spy``, 6 | /// but is exposed publicly to be composed with other types as desired. 7 | public final class DynamicResult { 8 | /// A value or closure to be used by the DynamicResult 9 | public enum Stub { 10 | /// A static value 11 | case value(Returning) 12 | /// A closure to be called. 13 | case closure(@Sendable (Arguments) -> Returning) 14 | 15 | /// Call the stub. 16 | /// If the stub is a `.value`, then return the value. 17 | /// If the stub is a `.closure`, then call the closure with the arguments. 18 | func call(_ arguments: Arguments) -> Returning { 19 | switch self { 20 | case .value(let returning): 21 | return returning 22 | case .closure(let closure): 23 | return closure(arguments) 24 | } 25 | } 26 | } 27 | 28 | private let lock = NSRecursiveLock() 29 | private var stubs: [Stub] 30 | 31 | private var _stubHistory: [Returning] = [] 32 | var stubHistory: [Returning] { 33 | lock.lock() 34 | defer { lock.unlock () } 35 | return _stubHistory 36 | } 37 | 38 | /// Create a new DynamicResult stubbed to return the values in the given order. 39 | /// That is, given `DynamicResult(1, 2, 3)`, 40 | /// if you call `.call` 5 times, you will get back `1, 2, 3, 3, 3`. 41 | public init(_ value: Returning, _ values: Returning...) { 42 | self.stubs = Array(value, values).map { Stub.value($0) } 43 | } 44 | 45 | internal init(_ values: [Returning]) { 46 | self.stubs = values.map { Stub.value($0) } 47 | } 48 | 49 | /// Create a new DynamicResult stubbed to call the given closure. 50 | public init(_ closure: @escaping @Sendable (Arguments) -> Returning) { 51 | self.stubs = [.closure(closure)] 52 | } 53 | 54 | /// Create a new DynamicResult stubbed to call the given stubs. 55 | public init(_ stub: Stub, _ stubs: Stub...) { 56 | self.stubs = Array(stub, stubs) 57 | } 58 | 59 | internal init(_ stubs: [Stub]) { 60 | self.stubs = stubs 61 | } 62 | 63 | /// Call the DynamicResult, returning the next stub in the list of stubs. 64 | public func call(_ arguments: Arguments) -> Returning { 65 | lock.lock() 66 | defer { lock.unlock () } 67 | let value = nextStub().call(arguments) 68 | _stubHistory.append(value) 69 | return value 70 | } 71 | 72 | /// Call the DynamicResult, returning the next stub in the list of stubs. 73 | public func call() -> Returning where Arguments == Void { 74 | call(()) 75 | } 76 | 77 | /// Replace the stubs with the new static values 78 | public func replace(_ value: Returning, _ values: Returning...) { 79 | replace(Array(value, values)) 80 | } 81 | 82 | /// Replace the stubs with the new static values 83 | internal func replace(_ values: [Returning]) { 84 | lock.lock() 85 | defer { lock.unlock () } 86 | self.resolvePendables() 87 | self.stubs = values.map { .value($0) } 88 | } 89 | 90 | /// Replace the stubs with the new closure. 91 | public func replace(_ closure: @escaping @Sendable (Arguments) -> Returning) { 92 | lock.lock() 93 | defer { lock.unlock () } 94 | self.resolvePendables() 95 | self.stubs = [.closure(closure)] 96 | } 97 | 98 | /// Replace the stubs with the new list of stubs 99 | public func replace(_ stub: Stub, _ stubs: Stub...) { 100 | lock.lock() 101 | defer { lock.unlock () } 102 | self.resolvePendables() 103 | self.stubs = Array(stub, stubs) 104 | } 105 | 106 | /// Replace the stubs with the new list of stubs 107 | internal func replace(_ stubs: [Stub]) { 108 | lock.lock() 109 | defer { lock.unlock () } 110 | self.resolvePendables() 111 | self.stubs = stubs 112 | } 113 | 114 | /// Append the values to the list of stubs. 115 | public func append(_ value: Returning, _ values: Returning...) { 116 | append(Array(value, values)) 117 | } 118 | 119 | internal func append(_ values: [Returning]) { 120 | lock.lock() 121 | defer { lock.unlock () } 122 | stubs.append(contentsOf: values.map { .value($0) }) 123 | } 124 | 125 | /// Append the closure to the list of stubs. 126 | public func append(_ closure: @escaping @Sendable (Arguments) -> Returning) { 127 | lock.lock() 128 | defer { lock.unlock () } 129 | stubs.append(.closure(closure)) 130 | } 131 | 132 | /// Append the stubs to the list of stubs. 133 | public func append(_ stub: Stub, _ stubs: Stub...) { 134 | append(Array(stub, stubs)) 135 | } 136 | 137 | internal func append(_ stubs: [Stub]) { 138 | lock.lock() 139 | defer { lock.unlock () } 140 | self.stubs.append(contentsOf: stubs) 141 | } 142 | 143 | private func nextStub() -> Stub { 144 | guard let stub = stubs.first else { 145 | fatalError("Fakes: DynamicResult \(self) has 0 stubs. This should never happen. File a bug at https://github.com/Quick/swift-fakes/issues/new") 146 | } 147 | if stubs.count > 1 { 148 | stubs.removeFirst() 149 | } 150 | return stub 151 | } 152 | 153 | private func resolvePendables() { 154 | stubs.forEach { 155 | guard case .value(let value) = $0 else { return } 156 | if let resolvable = value as? ResolvableWithFallback { 157 | resolvable.resolveWithFallback() 158 | } 159 | } 160 | } 161 | } 162 | 163 | extension DynamicResult: @unchecked Sendable where Arguments: Sendable, Returning: Sendable {} 164 | 165 | internal extension Array { 166 | init(_ value: Element, _ values: [Element]) { 167 | self = [value] + values 168 | } 169 | 170 | mutating func append(_ value: Element, _ values: [Element]) { 171 | self.append(contentsOf: Array(value, values)) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Sources/Fakes/Spy/Spy+ThrowingPendable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias ThrowingPendableSpy< 4 | Arguments, 5 | Success, 6 | Failure: Error 7 | > = Spy< 8 | Arguments, 9 | Pendable< 10 | Result< 11 | Success, 12 | Failure 13 | > 14 | > 15 | > 16 | 17 | extension Spy { 18 | /// Create a throwing pendable Spy that is pre-stubbed to return a pending that will block for a bit before returning success. 19 | public convenience init(pendingSuccess: Success) where Returning == ThrowingPendable { 20 | self.init(.pending(fallback: .success(pendingSuccess))) 21 | } 22 | 23 | /// Create a throwing pendable Spy that is pre-stubbed to return a pending that will block for a bit before returning Void. 24 | public convenience init() where Returning == ThrowingPendable<(), Failure> { 25 | self.init(.pending(fallback: .success(()))) 26 | } 27 | 28 | /// Create a throwing pendable Spy that is pre-stubbed to return a pending that will block for a bit before throwing an error. 29 | public convenience init(pendingFailure: Failure) where Returning == ThrowingPendable { 30 | self.init(.pending(fallback: .failure(pendingFailure))) 31 | } 32 | 33 | /// Create a throwing pendable Spy that is pre-stubbed to return a pending that will block for a bit before throwing an error 34 | public convenience init() where Returning == ThrowingPendable { 35 | self.init(.pending(fallback: .failure(EmptyError()))) 36 | } 37 | 38 | /// Create a throwing pendable Spy that is pre-stubbed to return a finished & successful value. 39 | public convenience init(success: Success) where Returning == ThrowingPendable { 40 | self.init(.finished(.success(success))) 41 | } 42 | 43 | /// Create a throwing pendable Spy that is pre-stubbed to throw the given error. 44 | public convenience init(failure: Failure) where Returning == ThrowingPendable { 45 | self.init(.finished(.failure(failure))) 46 | } 47 | } 48 | 49 | extension Spy { 50 | /// Resolve the pendable Spy's stub with the success value. 51 | public func resolveStub(success: Success) where Returning == ThrowingPendable { 52 | self.resolveStub(with: .success(success)) 53 | } 54 | 55 | /// Resolve the pendable spy's stub with the given error 56 | public func resolveStub(failure: Failure) where Returning == ThrowingPendable { 57 | self.resolveStub(with: .failure(failure)) 58 | } 59 | } 60 | 61 | extension Spy { 62 | /// Update the pendable Spy's stub to be in a pending state. 63 | public func stub(pendingSuccess: Success) where Returning == ThrowingPendable { 64 | self.stub(.pending(fallback: .success(pendingSuccess))) 65 | } 66 | 67 | /// Update the pendable Spy's stub to be in a pending state. 68 | public func stub(pendingFailure: Failure) where Returning == ThrowingPendable { 69 | self.stub(.pending(fallback: .failure(pendingFailure))) 70 | } 71 | 72 | /// Update the pendable Spy's stub to be in a pending state with a default failure value. 73 | public func stubPendingFailure() where Returning == ThrowingPendable { 74 | self.stub(pendingFailure: EmptyError()) 75 | } 76 | 77 | /// Update the throwing pendable Spy's stub to be successful, with the given value. 78 | /// 79 | /// - parameter success: The value to return when `callAsFunction` is called. 80 | public func stub(success: Success) where Returning == ThrowingPendable { 81 | self.stub(.finished(.success(success))) 82 | } 83 | 84 | /// Update the throwing pendable Spy's stub to throw the given error. 85 | /// 86 | /// - parameter failure: The error to throw when `callAsFunction` is called. 87 | public func stub(failure: Failure) where Returning == ThrowingPendable { 88 | self.stub(.finished(.failure(failure))) 89 | } 90 | 91 | /// Update the throwing pendable Spy's stub to throw an error. 92 | public func stubFailure() where Returning == ThrowingPendable { 93 | self.stub(failure: EmptyError()) 94 | } 95 | } 96 | 97 | extension Spy { 98 | // Returning == ThrowingPendable 99 | /// Records the arguments and handles the result according to ``Pendable/call(fallbackDelay:)``. 100 | /// This call then throws or returns the success, according to `Result.get`. 101 | /// 102 | /// - parameter arguments: The arguments to record. 103 | /// - parameter pendingDelay: The amount of seconds to delay if the `Pendable` is .pending before 104 | /// throwing a `PendableInProgressError`. If the `Pendable` is .finished, then this value is ignored. 105 | public func callAsFunction( 106 | _ arguments: Arguments, 107 | fallbackDelay: TimeInterval = PendableDefaults.delay 108 | ) async throws -> Success where Returning == ThrowingPendable { 109 | return try await call(arguments).call(fallbackDelay: fallbackDelay).get() 110 | } 111 | 112 | /// Records that a call was made and handles the result according to ``Pendable/call(fallbackDelay:)``. 113 | /// This call then throws or returns the success, according to `Result.get`. 114 | /// 115 | /// - parameter pendingDelay: The amount of seconds to delay if the `Pendable` is .pending before 116 | /// throwing a `PendableInProgressError`. If the `Pendable` is .finished, then this value is ignored. 117 | public func callAsFunction( 118 | fallbackDelay: TimeInterval = PendableDefaults.delay 119 | ) async throws -> Success where Arguments == Void, Returning == ThrowingPendable { 120 | return try await call(()).call(fallbackDelay: fallbackDelay).get() 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/Fakes/Pendable/Pendable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A way to type-erase Pendable, specifically just for resolving with the fallback. 4 | /// 5 | /// This is meant to be used by ```Spy``` as part of changing the stub in order to quickly resolve pending calls. 6 | public protocol ResolvableWithFallback { 7 | func resolveWithFallback() 8 | } 9 | 10 | /// Pendable is a safe way to represent the 2 states that an asynchronous call can be in 11 | /// 12 | /// - `pending`, the state while waiting for the call to finish. 13 | /// - `finished`, the state once the call has finished. 14 | /// 15 | /// Pendable allows you to finish a pending call after it's been made. This makes Pendable behave very 16 | /// similarly to something like Combine's `Future`. 17 | /// 18 | /// - Note: The reason you must provide a fallback value is to prevent deadlock when used in test. 19 | /// Unlike something like Combine's `Future`, it is very often the case that you will write 20 | /// tests which end while the call is in the pending state. If you do this too much, then your 21 | /// entire test suite will deadlock, as Swift Concurrency works under the assumption that 22 | /// blocked tasks of work will always eventually be unblocked. To help prevent this, pending calls 23 | /// are always resolved with the fallback after a given delay. You can also manually force this 24 | /// by calling the ``Pendable\resolveWithFallback()`` method. 25 | public final class Pendable: @unchecked Sendable, ResolvableWithFallback { 26 | private enum State: Sendable { 27 | case pending 28 | case finished(Value) 29 | } 30 | 31 | private let lock = NSRecursiveLock() 32 | private var state = State.pending 33 | 34 | private var inProgressCalls = [UnsafeContinuation]() 35 | 36 | private let fallbackValue: Value 37 | 38 | private var currentValue: Value { 39 | switch state { 40 | case .pending: 41 | return fallbackValue 42 | case .finished(let value): 43 | return value 44 | } 45 | } 46 | 47 | deinit { 48 | resolveWithFallback() 49 | } 50 | 51 | /// Initializes a new `Pendable`, in a pending state, with the given fallback value. 52 | public init(fallbackValue: Value) { 53 | self.fallbackValue = fallbackValue 54 | } 55 | 56 | /// Gets the value for the `Pendable`, possibly waiting until it's resolved. 57 | /// 58 | /// - parameter fallbackDelay: The amount of time (in seconds) to wait until the call returns 59 | /// the fallback value. This is only used when the `Pendable` is in a pending state. 60 | public func call(fallbackDelay: TimeInterval = PendableDefaults.delay) async -> Value { 61 | return await withTaskGroup(of: Value.self) { taskGroup in 62 | taskGroup.addTask { await self.handleCall() } 63 | taskGroup.addTask { await self.resolveAfterDelay(fallbackDelay) } 64 | 65 | guard let value = await taskGroup.next() else { 66 | fatalError("There were no tasks in the task group. This should not ever happen.") 67 | } 68 | taskGroup.cancelAll() 69 | return value 70 | 71 | } 72 | } 73 | 74 | /// Resolves the `Pendable` with the fallback value. 75 | /// 76 | /// - Note: This no-ops if the pendable is already in a resolved state. 77 | /// - Note: This is called for when you re-stub a `Spy` in ``Spy/stub(_:)`` 78 | public func resolveWithFallback() { 79 | lock.lock() 80 | defer { lock.unlock() } 81 | 82 | if case .pending = state { 83 | resolve(with: fallbackValue) 84 | } 85 | } 86 | 87 | /// Resolves the `Pendable` with the given value. 88 | /// 89 | /// Even if the pendable is already resolves, this resets the resolved value to the given value. 90 | public func resolve(with value: Value) { 91 | lock.lock() 92 | defer { lock.unlock() } 93 | state = .finished(value) 94 | inProgressCalls.forEach { 95 | $0.resume(returning: value) 96 | } 97 | inProgressCalls = [] 98 | 99 | } 100 | 101 | /// Resolves any outstanding calls to the `Pendable` with the current value, 102 | /// and resets it back into the pending state. 103 | public func reset() { 104 | lock.lock() 105 | defer { lock.unlock() } 106 | 107 | inProgressCalls.forEach { 108 | $0.resume(returning: currentValue) 109 | } 110 | inProgressCalls = [] 111 | state = .pending 112 | } 113 | 114 | // MARK: - Private 115 | private func handleCall() async -> Value { 116 | return await withUnsafeContinuation { continuation in 117 | lock.lock() 118 | defer { lock.unlock() } 119 | switch state { 120 | case .pending: 121 | inProgressCalls.append(continuation) 122 | case .finished(let value): 123 | continuation.resume(returning: value) 124 | } 125 | } 126 | } 127 | 128 | private func resolveAfterDelay(_ delay: TimeInterval) async -> Value { 129 | do { 130 | try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) 131 | } catch {} 132 | resolveWithFallback() 133 | return fallbackValue 134 | } 135 | } 136 | 137 | @available(*, deprecated, renamed: "ThrowingPendable") 138 | public typealias ThrowingDynamicPendable = Pendable> 139 | 140 | public typealias ThrowingPendable = Pendable> 141 | 142 | extension Pendable { 143 | /// Gets or throws value for the `Pendable`, possibly waiting until it's resolved. 144 | /// 145 | /// - parameter resolveDelay: The amount of time (in seconds) to wait until the call returns 146 | /// the fallback value. This is only used when the `Pendable` is in a pending state. 147 | public func call( 148 | resolveDelay: TimeInterval = PendableDefaults.delay 149 | ) async throws -> Success where Value == Result { 150 | try await call(fallbackDelay: resolveDelay).get() 151 | } 152 | } 153 | 154 | extension Pendable { 155 | /// Creates a new finished `Pendable` pre-resolved with the given value. 156 | public static func finished(_ value: Value) -> Pendable { 157 | let pendable = Pendable(fallbackValue: value) 158 | pendable.resolve(with: value) 159 | return pendable 160 | } 161 | 162 | /// Creates a new finished `Pendable` pre-resolved with Void. 163 | public static func finished() -> Pendable where Value == Void { 164 | return Pendable.finished(()) 165 | } 166 | } 167 | 168 | extension Pendable { 169 | /// Creates a new pending `Pendable` with the given fallback value. 170 | public static func pending(fallback: Value) -> Pendable { 171 | Pendable(fallbackValue: fallback) 172 | } 173 | 174 | /// Creates a new pending `Pendable` with a fallback value of Void. 175 | public static func pending() -> Pendable where Value == Void { 176 | Pendable(fallbackValue: ()) 177 | } 178 | 179 | /// Creates a new pending `Pendable` with a fallback value of nil. 180 | public static func pending() -> Pendable where Value == Optional { 181 | // swiftlint:disable:previous syntactic_sugar 182 | Pendable(fallbackValue: nil) 183 | } 184 | 185 | /// Creatse a new pending `Pendable` with a fallback value of an error. 186 | public static func pending() -> Pendable where Value == Result { 187 | Pendable(fallbackValue: Result.failure(EmptyError())) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Tutorials/WritingFakes/RecipeService.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorial(time: 20) { 2 | @Intro(title: "The RecipeService object") { 3 | Testing an object to download recipes and upload new ones. 4 | } 5 | 6 | @Section(title: "Writing the synchronous Fake") { 7 | @ContentAndMedia { 8 | Suppose you are working on an application to view and write recipes. 9 | 10 | As part of this, there is a server component you need to interface with 11 | in order to download recipes and upload new ones. 12 | } 13 | 14 | @Steps { 15 | @Step { 16 | Let's first start with the `NetworkInterface` protocol. 17 | 18 | This is a protocol for a `URLSession`-like object that can make 19 | various network requests. 20 | 21 | @Code(name: "NetworkInterface.swift", file: "NetworkInterface-01.swift") 22 | } 23 | 24 | @Step { 25 | Given this `NetworkInterface`, you might write `RecipeService` 26 | like so: 27 | 28 | @Code(name: "RecipeService.swift", file: "RecipeService-01.swift") 29 | } 30 | 31 | @Step { 32 | In writing the tests for your RecipeService class, 33 | you could just use the real implementation of `NetworkInterface`. But now your 34 | tests require you to have a reliable connection not just to the internet at 35 | large, but also to the particular target server in order to work. If that server 36 | ever goes down for maintenance, your tests will just fail. Additionally, using 37 | a real version of `NetworkInterface` means you can only semi-reliably test the 38 | happy paths. Good luck writing tests of any error cases. 39 | 40 | Given that, you decide to create a Fake and inject it (using 41 | ) to your Subject. You decide to come up with this 42 | first attempt at `FakeNetworkInterface`. 43 | 44 | @Code(name: "FakeNetworkInterface.swift", file: "FakeNetworkInterface-01.swift") 45 | } 46 | 47 | @Step { 48 | But this isn't very useful. For one, there's no way to control the result of 49 | `get(from:)`. For another, there's no way to even check that your fake is even 50 | used. This is where ``Spy`` comes in. Let's rewrite `FakeNetworkInterface` using 51 | ``Spy``. 52 | 53 | @Code(name: "FakeNetworkInterface.swift", file: "FakeNetworkInterface-02.swift") 54 | } 55 | 56 | @Step { 57 | This is the start of everything we need to write tests for 58 | `RecipeService`, so let's do just that. 59 | 60 | @Code(name: "RecipeServiceTests.swift", file: "RecipeServiceTests-01.swift") 61 | } 62 | } 63 | } 64 | 65 | @Section(title: "Updating for Throwing") { 66 | @ContentAndMedia { 67 | Actual network calls can fail, and just crashing the app 68 | just because the network fails is a bad experience at best. Luckily, 69 | `Spy` can handle throwing calls. So let's update our code to handle 70 | network errors. 71 | } 72 | 73 | @Steps { 74 | @Step { 75 | First, let's start with updating `NetworkInterface`. 76 | 77 | @Code(name: "NetworkInterface.swift", file: "NetworkInterface-02.swift") 78 | } 79 | 80 | @Step { 81 | With the updated `NetworkInterface`, we also have to update 82 | `RecipeService` to handle these new errors. For our purposes, 83 | let's just rethrow the errors and not do anything. 84 | 85 | @Code(name: "RecipeService.swift", file: "RecipeService-02.swift") 86 | } 87 | 88 | @Step { 89 | Updating `NetworkInterface` also requires us to update 90 | `FakeNetworkInterface`. 91 | There are two ways we could do this: Use 92 | `Spy>`, or use the typealias 93 | `ThrowingSpy`. 94 | `ThrowingSpy` is less boilerplate, and therefore easier to 95 | understand, so we'll go with that. 96 | 97 | @Code(name: "FakeNetworkInterface.swift", file: "FakeNetworkInterface-03.swift") 98 | } 99 | 100 | @Step { 101 | Now that we can throw errors in test, we really should add tests 102 | of what happens when `NetworkInterface` throws an error. 103 | This particular case is rather boring, but it is good to verify 104 | that we're not crashing on errors. A downstream object might 105 | take the error and present an alert with a retry option. 106 | 107 | @Code(name: "RecipeServiceTests.swift", file: "RecipeServiceTests-02.swift") 108 | } 109 | } 110 | } 111 | 112 | @Section(title: "Handling Asynchronous Calls") { 113 | @ContentAndMedia { 114 | In practice, it's also really bad practice to make synchronous 115 | network calls. Doing so requires us to remember to call the 116 | `NetworkInterface` from a background thread, or else risk blocking 117 | the main thread. Which is, again, a poor experience at best, and 118 | very likely to get you an `8BADF00D` crash. Better to make 119 | network calls asynchronous at the source - meaning at 120 | `NetworkInterface`. 121 | } 122 | 123 | @Steps { 124 | @Step { 125 | As with before, let's start with `NetworkInterface`, making use 126 | of Swift Concurrency for this. 127 | 128 | @Code(name: "NetworkInterface.swift", file: "NetworkInterface-03.swift") 129 | } 130 | 131 | @Step { 132 | With the again-updated `NetworkInterface`, we once again have 133 | to update `RecipeService` to work with the asynchronous 134 | `NetworkInterface`. 135 | 136 | @Code(name: "RecipeService.swift", file: "RecipeService-03.swift") 137 | } 138 | 139 | @Step { 140 | And again, we turn our attention to `FakeNetworkInterface`. 141 | We could update the Spies to be any of 142 | `Spy<..., Pendable>`, 143 | `Spy<..., ThrowingPendable>`, 144 | `PendableSpy<..., Result>`, or 145 | `ThrowingPendableSpy<..., Success, Error>`. As with before, 146 | let's use the one that flattens the generics as much as 147 | possible: `ThrowingPendableSpy`. 148 | 149 | @Code(name: "FakeNetworkInterface.swift", file: "FakeNetworkInterface-04.swift") 150 | } 151 | 152 | @Step { 153 | Finally, we need to update the tests to work with all these 154 | async calls. 155 | 156 | @Code(name: "RecipeServiceTests.swift", file: "RecipeServiceTests-03.swift") 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Tests/FakesTests/SpyTests.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import XCTest 3 | @testable import Fakes 4 | 5 | final class SpyTests: XCTestCase { 6 | func testVoid() { 7 | let subject = Spy() 8 | 9 | // it returns the returning argument as the result, without stubbing. 10 | expect { 11 | subject(()) 12 | }.to(beVoid()) 13 | 14 | // it records the call 15 | expect(subject.calls).to(haveCount(1)) 16 | expect(subject.calls.first).to(beVoid()) 17 | 18 | // allows Void arguments to be called without passing in a void 19 | expect { 20 | subject() 21 | }.to(beVoid()) 22 | 23 | // it records the call. 24 | expect(subject.calls).to(haveCount(2)) 25 | expect(subject.calls.last).to(beVoid()) 26 | } 27 | 28 | func testStubbing() { 29 | let subject = Spy(123) 30 | 31 | expect { 32 | subject() 33 | }.to(equal(123)) 34 | 35 | subject.stub(456) 36 | 37 | expect { 38 | subject() 39 | }.to(equal(456)) 40 | } 41 | 42 | func testMultipleStubs() { 43 | let subject = Spy(1, 2, 3) 44 | 45 | expect(subject()).to(equal(1)) 46 | expect(subject()).to(equal(2)) 47 | expect(subject()).to(equal(3)) 48 | expect(subject()).to(equal(3)) 49 | } 50 | 51 | func testClosureStubs() { 52 | let subject = Spy { $0 } 53 | 54 | expect(subject(1)).to(equal(1)) 55 | expect(subject(2)).to(equal(2)) 56 | expect(subject(3)).to(equal(3)) 57 | expect(subject(10)).to(equal(10)) 58 | } 59 | 60 | func testReplacingStubs() { 61 | let subject = Spy(5) 62 | subject.stub(1, 2, 3) 63 | 64 | expect(subject()).to(equal(1)) 65 | expect(subject()).to(equal(2)) 66 | expect(subject()).to(equal(3)) 67 | expect(subject()).to(equal(3)) 68 | } 69 | 70 | func testReplacingClosures() { 71 | let subject = Spy(5) 72 | subject.stub { $0 } 73 | 74 | expect(subject(1)).to(equal(1)) 75 | expect(subject(2)).to(equal(2)) 76 | expect(subject(3)).to(equal(3)) 77 | expect(subject(10)).to(equal(10)) 78 | } 79 | 80 | func testResult() { 81 | let subject = Spy>(.failure(TestError.uhOh)) 82 | 83 | expect { 84 | try subject() as Int 85 | }.to(throwError(TestError.uhOh)) 86 | 87 | expect { 88 | try subject(()) as Int 89 | }.to(throwError(TestError.uhOh)) 90 | 91 | // stub(success:) 92 | 93 | subject.stub(success: 2) 94 | 95 | expect { 96 | try subject() 97 | }.to(equal(2)) 98 | 99 | // stub(failure:) 100 | 101 | subject.stub(failure: .ohNo) 102 | 103 | expect { 104 | try subject() as Int 105 | }.to(throwError(TestError.ohNo)) 106 | } 107 | 108 | func testResultInitializers() { 109 | let subject = ThrowingSpy(failure: .ohNo) 110 | 111 | expect { 112 | try subject() as Int 113 | }.to(throwError(TestError.ohNo)) 114 | 115 | let subject2 = ThrowingSpy(success: 3) 116 | expect { 117 | try subject2() 118 | }.to(equal(3)) 119 | } 120 | 121 | func testResultTakesNonVoidArguments() { 122 | let intSpy = Spy() 123 | 124 | intSpy(1) 125 | 126 | expect(intSpy.calls).to(equal([1])) 127 | 128 | intSpy(1) 129 | } 130 | 131 | func testPendable() async { 132 | let subject = PendableSpy(pendingFallback: 1) 133 | 134 | await expect { 135 | await subject(fallbackDelay: 0) 136 | }.toEventually(equal(1)) 137 | 138 | subject.stub(finished: 4) 139 | 140 | await expect { 141 | await subject(fallbackDelay: 0) 142 | }.toEventually(equal(4)) 143 | } 144 | 145 | func testPendableTakesNonVoidArguments() async throws { 146 | let subject = PendableSpy(finished: ()) 147 | 148 | await subject(3, fallbackDelay: 0) 149 | 150 | expect(subject.calls).to(equal([3])) 151 | } 152 | 153 | func testThrowingPendable() async { 154 | let subject = ThrowingPendableSpy(pendingSuccess: 0) 155 | 156 | await expect { 157 | try await subject(fallbackDelay: 0) 158 | }.toEventually(equal(0)) 159 | 160 | subject.stub(success: 5) 161 | 162 | await expect { 163 | try await subject(fallbackDelay: 0) 164 | }.toEventually(equal(5)) 165 | 166 | subject.stub(failure: TestError.uhOh) 167 | await expect { 168 | try await subject(fallbackDelay: 0) 169 | }.toEventually(throwError(TestError.uhOh)) 170 | } 171 | 172 | func testThrowingPendableTakesNonVoidArguments() async throws { 173 | let subject = ThrowingPendableSpy(success: ()) 174 | 175 | try await subject(8, fallbackDelay: 0) 176 | 177 | expect(subject.calls).to(equal([8])) 178 | } 179 | 180 | func testClearCalls() { 181 | let subject = Spy() 182 | 183 | subject(1) 184 | subject(2) 185 | 186 | subject.clearCalls() 187 | expect(subject.calls).to(beEmpty()) 188 | } 189 | 190 | func testDynamicPendable() async { 191 | let subject = Spy>() 192 | 193 | let managedTask = await ManagedTask.running { 194 | await subject() 195 | } 196 | 197 | await expect { await managedTask.isFinished }.toNever(beTrue()) 198 | 199 | subject.resolveStub(with: ()) 200 | 201 | await expect { await managedTask.isFinished }.toEventually(beTrue()) 202 | } 203 | 204 | func testDynamicPendableDeinit() async { 205 | let subject = Spy>() 206 | 207 | let managedTask = await ManagedTask.running { 208 | await subject() 209 | } 210 | 211 | await expect { await managedTask.hasStarted }.toEventually(beTrue()) 212 | 213 | subject.stub(Pendable.pending()) 214 | subject.resolveStub(with: ()) 215 | 216 | await expect { await managedTask.isFinished }.toEventually(beTrue()) 217 | } 218 | } 219 | 220 | actor ManagedTask { 221 | var hasStarted = false 222 | var isFinished = false 223 | 224 | var task: Task! 225 | 226 | static func running(closure: @escaping @Sendable () async throws -> Success) async -> ManagedTask where Failure == Error { 227 | let task = ManagedTask() 228 | 229 | await task.run(closure: closure) 230 | 231 | return task 232 | } 233 | 234 | static func running(closure: @escaping @Sendable () async -> Success) async -> ManagedTask where Failure == Never { 235 | let task = ManagedTask() 236 | 237 | await task.run(closure: closure) 238 | 239 | return task 240 | } 241 | 242 | private init() {} 243 | 244 | private func run(closure: @escaping @Sendable () async throws -> Success) where Failure == Error { 245 | task = Task { 246 | self.recordStarted() 247 | let result = try await closure() 248 | self.recordFinished() 249 | return result 250 | } 251 | } 252 | 253 | private func run(closure: @escaping @Sendable () async -> Success) where Failure == Never { 254 | task = Task { 255 | self.recordStarted() 256 | let result = await closure() 257 | self.recordFinished() 258 | return result 259 | } 260 | } 261 | 262 | private func recordStarted() { 263 | self.hasStarted = true 264 | } 265 | 266 | private func recordFinished() { 267 | self.isFinished = true 268 | } 269 | 270 | var result: Result { 271 | get async { 272 | await task.result 273 | } 274 | } 275 | 276 | var value: Success { 277 | get async throws { 278 | try await task.value 279 | } 280 | } 281 | } 282 | 283 | enum TestError: Error { 284 | case uhOh 285 | case ohNo 286 | } 287 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024, Quick Team 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Guides/VerifyingCallbacks.md: -------------------------------------------------------------------------------- 1 | # Verifying Callbacks and Faking DispatchQueue 2 | 3 | When testing a method that calls a callback, how do you verify that the 4 | callback actually works? 5 | 6 | ## Contents 7 | 8 | Let's say we're testing a method that uses a callback as part of its behavior. 9 | How would we verify that the callback actually gets called? 10 | 11 | To examine that, let's look at a function that does some expensive computation. 12 | This function uses `DispatchQueue` to do that computation on a background 13 | thread. To let the caller know that the work has finished, this function takes 14 | in a completion handler in the form of a callback. 15 | 16 | Let's say that method looks something like this: 17 | 18 | ```swift 19 | func expensiveComputation(completionHandler: @escaping (Int) -> Void) { 20 | DispatchQueue.global().async { 21 | // ... do some work, to compute a value 22 | let value: Int = 1337 // for the sake of example. 23 | completionHandler(value) 24 | } 25 | } 26 | ``` 27 | 28 | In that case, we could pass a closure that calls a ``Spy`` with the 29 | completion handler's arguments: 30 | 31 | ```swift 32 | final class ExpensiveComputationTests: XCTestCase { 33 | func testCallsTheCompletionHandler() { 34 | let completionHandler = Spy() 35 | 36 | let expectation = self.expectation( 37 | description: "expensiveComputation completion handler" 38 | ) 39 | 40 | expensiveComputation { 41 | completionHandler.callAsFunction($0) 42 | expectation.fulfill() 43 | } 44 | 45 | self.waitForExpectations(timeout: 1) 46 | 47 | XCTAssertEqual(completionHandler.calls, [1]) 48 | } 49 | } 50 | ``` 51 | 52 | This has a lot of boilerplate. Setting up an `Expectation`, fulfilling it, 53 | and waiting for it to be called is exactly the kind of situation that 54 | [Nimble's Polling Expectations](https://quick.github.io/Nimble/documentation/nimble/pollingexpectations) 55 | were created to handle. 56 | 57 | Let's refactor the test using Polling Expectations: 58 | 59 | ```swift 60 | final class ExpensiveComputationTests: XCTestCase { 61 | func testCallsTheCallback() { 62 | let completionHandler = Spy() 63 | 64 | expensiveComputation(completionHandler: completionHandler.callAsFunction) 65 | 66 | expect(completionHandler).toEventually(beCalled(1337)) 67 | } 68 | } 69 | ``` 70 | 71 | ### Writing Fakes that use Callbacks 72 | 73 | Of course, even better than waiting for the completion handler to eventually be 74 | called, is being able to control when it's called. 75 | 76 | > Note: This section assumes you're familiar with writing fakes and injecting 77 | them to the subject using Dependency Injection. See 78 | for a refresher. 79 | 80 | Because `DispatchQueue` is an open class, we could write a subclass that 81 | overrides its public API and does the right thing. That's doable, but 82 | [`DispatchQueue`](https://developer.apple.com/documentation/dispatch/dispatchqueue) 83 | has a very large public API, and our subclass would have to implement all of that 84 | plus keep up with any changes Apple makes in the future to avoid accidentally 85 | calling into the base DispatchQueue class. 86 | 87 | Instead, whenever possible, you should favor composition over inheritance. 88 | Which, for this case, means we'll write a protocol to wrap `DispatchQueue`. 89 | 90 | We're only calling `async` with the default arguments, so our protocol can 91 | be a single method that takes the `execute` argument: 92 | 93 | ```swift 94 | protocol DispatchQueueProtocol { 95 | func async(execute work: @escaping @Sendable () -> Void) 96 | } 97 | 98 | extension DispatchQueue: DispatchQueueProtocol { 99 | func async(execute work: @escaping @Sendable () -> Void) { 100 | self.async(group: nil, execute: work) 101 | } 102 | } 103 | ``` 104 | 105 | Our `DispatchQueueProtocol` is only using one of the arguments to DispatchQueue's [`async(group:qos:flags:execute:)`](https://developer.apple.com/documentation/dispatch/dispatchqueue/2016098-async), 106 | so the protocol wrapping `DispatchQueue` only really needs the `execute` 107 | argument. Because default arguments don't really play well with protocols, we 108 | need to manually implement `async(execute:)`, and also specify one of the 109 | default arguments to prevent recursively calling `async(execute:)`. 110 | 111 | Now, we can inject our `DispatchQueueProtocol`: 112 | 113 | ```swift 114 | func expensiveComputation(dispatchQueue: DispatchQueueProtocol, completionHandler: @escaping (Int) -> Void) { 115 | dispatchQueue.async { 116 | // ... do some work, to compute a value 117 | let value: Int = 1337 // for the sake of example. 118 | completionHandler(value) 119 | } 120 | } 121 | ``` 122 | 123 | and then turn our attention to to implementing `FakeDispatchQueue`. 124 | 125 | Following the example from , we will implement 126 | `FakeDispatchQueue` using only `Spy`: 127 | 128 | ```swift 129 | final class FakeDispatchQueue: DispatchQueueProtocol { 130 | let asyncSpy = Spy<@Sendable () -> Void, Void>() 131 | func async(execute work: @escaping @Sendable () -> Void) { 132 | asyncSpy(work) 133 | } 134 | } 135 | ``` 136 | 137 | This will work similarly to when we used a Spy as the completion handler to 138 | `expensiveComputation(completionHandler:)` earlier. 139 | 140 | Now we use `FakeDispatchQueue` in our tests for `expensiveComputation`: 141 | 142 | ```swift 143 | final class ExpensiveComputationTests: XCTestCase { 144 | func testCallsTheCallback() throws { 145 | let dispatchQueue = FakeDispatchQueue() 146 | let completionHandler = Spy() 147 | 148 | expensiveComputation( 149 | dispatchQueue: dispatchQueue, 150 | completionHandler: completionHandler.callAsFunction 151 | ) 152 | 153 | try require(dispatchQueue.asyncSpy).to(beCalled()) 154 | 155 | dispatchQueue.asyncSpy.calls.last?() // call the recorded completion handler. 156 | 157 | expect(completionHandler).to(beCalled(1337)) 158 | } 159 | } 160 | ``` 161 | 162 | This has quite a bit more boilerplate than simply using 163 | `expect(completionHandler).toEventually(beCalled(...))`. However, the benefit of 164 | this boilerplate is that this test runs significantly faster. We no longer have 165 | to wait for the system to decide to run the closure that calls the completion 166 | handler, which results in a small reduction in test runtime. 167 | 168 | Additionally, we can also control when this closure is run, which allows us 169 | to verify any other behavior that might be happening before the code is 170 | run in the background, or while code is run in the background. 171 | 172 | ### Improving FakeDispatchQueue 173 | 174 | The current implementation of `FakeDispatchQueue` still leaves a lot to be 175 | desired. Yes, it works, but it's clunky. Having to first require that `async` 176 | was called, then call the last call recorded is a lot of typing. Being able to 177 | control when closures run is great, but having to go through ``Spy`` makes it 178 | really easy to either run the same closure twice, or not run one that should 179 | have run. 180 | 181 | To address this, we'll flesh out `FakeDispatchQueue` 182 | some more, adding some logic to record closures, call them at a later time, or 183 | run the closure when `async(execute:)` is called. 184 | 185 | ```swift 186 | import Foundation 187 | 188 | final class FakeDispatchQueue: DispatchQueueProtocol, @unchecked Sendable { 189 | private let lock = NSRecursiveLock() 190 | private var calls = [@Sendable () -> Void]() 191 | 192 | func pendingTaskCount() -> Int { 193 | lock.withLock { calls.count } 194 | } 195 | 196 | func runNextTask() { 197 | lock.withLock { 198 | guard !calls.isEmpty else { return } 199 | calls.removeFirst()() 200 | } 201 | } 202 | 203 | func async(execute work: @escaping @Sendable () -> Void) { 204 | lock.withLock { 205 | guard _runSynchronously == false else { 206 | return work() 207 | } 208 | calls.append(work) 209 | } 210 | } 211 | } 212 | ``` 213 | 214 | Before we can use this new `FakeDispatchQueue`, it's very important to realize 215 | that, because it now has logic in it, we have to [write tests for that logic](https://blog.rachelbrindle.com/2023/06/25/testing-test-helpers/). 216 | Otherwise, we have no idea if a test failure is caused by something 217 | in the production code, or something in `FakeDispatchQueue`. 218 | 219 | Some tests that verify the entire API for `FakeDispatchQueue` look like this: 220 | 221 | ```swift 222 | final class FakeDispatchQueueTests: XCTestCase { 223 | func testRunNextTaskWithoutPendingTasksDoesntBlowUp() { 224 | let subject = FakeDispatchQueue() 225 | 226 | expect { subject.runNextTask() }.toNot(throwAssertion()) 227 | } 228 | 229 | func testRunNextTaskWithPendingTasksCallsTheCallback() { 230 | let subject = FakeDispatchQueue() 231 | let spy = Spy() 232 | 233 | subject.async { spy.call() } 234 | 235 | expect(spy).toNot(beCalled()) 236 | 237 | subject.runNextTask() 238 | 239 | expect(spy).to(beCalled()) 240 | } 241 | 242 | func testPendingTaskCountReturnsNumberOfUnRunTasks() { 243 | let subject = FakeDispatchQueue() 244 | let spy = Spy() 245 | 246 | expect(subject.pendingTaskCount).to(equal(0)) 247 | 248 | subject.async { spy.call() } 249 | 250 | expect(subject.pendingTaskCount).to(equal(1)) 251 | 252 | subject.async { spy.call() } 253 | 254 | expect(subject.pendingTaskCount).to(equal(2)) 255 | 256 | subject.runNextTask() 257 | 258 | expect(subject.pendingTaskCount).to(equal(1)) 259 | 260 | subject.runNextTask() 261 | 262 | expect(subject.pendingTaskCount).to(equal(0)) 263 | } 264 | 265 | func testPendingTasksAreRunInTheOrderReceived() { 266 | let subject = FakeDispatchQueue() 267 | let spy = Spy() 268 | 269 | subject.async { spy.call(1) } 270 | subject.async { spy.call(2) } 271 | 272 | subject.runNextTask() 273 | subject.runNextTask() 274 | 275 | expect(subject.calls).to(equal([1, 2])) 276 | } 277 | } 278 | ``` 279 | 280 | > Important: Any code that has logic in it must have tests driving out that logic. 281 | 282 | These tests do the basic of ensuring that tasks are queued up properly, and 283 | are actually ran in a queue. They make sure that we can correctly assert on 284 | the number of queued up tasks. And, most importantly, they ensure that if 285 | we try to run a task when there aren't any to run, the tests don't crash. 286 | 287 | Now, with `FakeDispatchQueue` implemented and tested, we can use it in 288 | `ExpensiveComputationTests`: 289 | 290 | ```swift 291 | final class ExpensiveComputationTests: XCTestCase { 292 | func testCallsTheCallback() { 293 | let dispatchQueue = FakeDispatchQueue() 294 | let completionHandler = Spy() 295 | 296 | expensiveComputation( 297 | dispatchQueue: dispatchQueue, 298 | completionHandler: completionHandler.callAsFunction 299 | ) 300 | 301 | expect(dispatchQueue.pendingTaskCount).to(equal(1)) 302 | 303 | dispatchQueue.runNextTask() 304 | 305 | expect(completionHandler).to(beCalled(1337)) 306 | } 307 | } 308 | ``` 309 | 310 | Which isn't that much different, but it's now significantly easier to understand 311 | what's actually going on. `runNextTask()` is so much easier to read than 312 | `asyncSpy.calls.last?()`, and it has way fewer sharp edges to cut yourself on. 313 | 314 | ### Conclusion 315 | 316 | In this article, we discussed verifying callbacks as well as designing and 317 | testing code using `DispatchQueue` to run code asynchronously. Verify callbacks 318 | by injecting a ``Spy``, and do whatever you can to control when dispatched code 319 | actually runs. 320 | 321 | Swift Concurrency (async/await) brings its own challenges, which will be 322 | addressed in a separate article. 323 | -------------------------------------------------------------------------------- /Sources/Fakes/Spy/Spy+Nimble.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | 3 | // MARK: - Verifying any calls to the Spy. 4 | 5 | /// A Nimble matcher for ``Spy`` that succeeds when any of the calls to the spy matches the given matchers. 6 | /// 7 | /// If no matchers are specified, then this matcher will succeed if the spy has been called at all. 8 | /// 9 | /// - parameter matchers: The matchers to run against the calls to spy to verify it has been called correctly 10 | /// - Note: If the Spy's `Arguments` is Equatable, you can use `BeCalled` with 11 | /// the expected value. This is the same as `beCalled(equal(expectedValue))`. 12 | /// - Note: All matchers for a single call must pass in order for this matcher 13 | /// to pass. Specifying multiple matchers DOES NOT verify multiple calls. 14 | /// Passing in multiple matchers is a shorthand for `beCalled(satisfyAllOf(...))`. 15 | /// - SeeAlso: ``mostRecentlyBeCalled(_:)-9i9t9`` for when you want to check that 16 | /// only the most recent call to the spy matches the matcher. 17 | public func beCalled( 18 | _ matchers: Matcher... 19 | ) -> Matcher> { 20 | let rawMessage: String 21 | if matchers.isEmpty { 22 | rawMessage = "be called" 23 | } else { 24 | rawMessage = "be called with \(matchers.count) matchers" 25 | } 26 | return _beCalled(rawMessage: rawMessage) { validatorArgs in 27 | if matchers.isEmpty { 28 | return MatcherResult( 29 | bool: validatorArgs.spy.calls.isEmpty == false, 30 | message: validatorArgs.message) 31 | } 32 | for call in validatorArgs.spy.calls { 33 | let matcherExpression = Expression( 34 | expression: { call }, 35 | location: validatorArgs.expression.location 36 | ) 37 | let results = try matchers.map { 38 | try $0.satisfies(matcherExpression) 39 | } 40 | if results.allSatisfy({ $0.toBoolean(expectation: .toMatch) }) { 41 | return MatcherResult( 42 | bool: true, 43 | message: validatorArgs.message.appended( 44 | details: results.map { 45 | $0.message.toString( 46 | actual: stringify(validatorArgs.spy) 47 | ) 48 | }.joined(separator: "\n") 49 | ) 50 | ) 51 | } 52 | } 53 | return MatcherResult(bool: false, message: validatorArgs.message) 54 | } 55 | } 56 | 57 | /// A nimble matcher for ``Spy`` that succeeds when the spy has been called 58 | /// `times` times, and it has been called at least once with arguments that match 59 | /// the matcher. 60 | /// 61 | /// If no matchers are given, then this matcher will succeed if the spy has 62 | /// been called exactly the number of times specified. 63 | /// 64 | /// For example, if your spy has been called a total of 4 times, and at least one of those times matching 65 | /// whatever your matcher is, then `beCalled(..., times: 4)` will match. 66 | /// However, `beCalled(..., times: 3)` will not match, because the matcher has been called 4 times. 67 | /// 68 | /// Alternatively, if your spy has been called a total of 4 times, and you pass in 0 matchers, then 69 | /// `beCalled(times: 4)` will match, regardless of what those calls are. 70 | /// 71 | /// - parameter matchers: The matchers used to search for matching calls. 72 | /// - parameter times: The expected amount of calls the Spy should have been called. 73 | /// 74 | /// - Note: All matchers for a single call must pass in order for this matcher 75 | /// to pass. Specifying multiple matchers DOES NOT verify multiple calls. 76 | /// Passing in multiple matchers is a shorthand for `beCalled(satisfyAllOf(...))`. 77 | public func beCalled( 78 | _ matchers: Matcher..., 79 | times: Int 80 | ) -> Matcher> { 81 | let rawMessage: String 82 | if matchers.isEmpty { 83 | rawMessage = "be called \(times) times" 84 | } else { 85 | rawMessage = "be called \(times) times, at least one of them matches \(matchers.count) matchers" 86 | } 87 | return _beCalled(rawMessage: rawMessage) { validatorArgs in 88 | let calls = validatorArgs.spy.calls 89 | 90 | if calls.count != times { 91 | return MatcherResult( 92 | bool: false, 93 | message: validatorArgs.message.appended( 94 | details: "but was called \(calls.count) times" 95 | ) 96 | ) 97 | } 98 | 99 | if matchers.isEmpty { 100 | return MatcherResult(bool: true, message: validatorArgs.message) 101 | } 102 | for call in calls { 103 | let matcherExpression = Expression( 104 | expression: { call }, 105 | location: validatorArgs.expression.location 106 | ) 107 | let results = try matchers.map { 108 | try $0.satisfies(matcherExpression) 109 | } 110 | if results.allSatisfy({ $0.toBoolean(expectation: .toMatch) }) { 111 | return MatcherResult( 112 | bool: true, 113 | message: validatorArgs.message.appended( 114 | details: results.map { 115 | $0.message.toString( 116 | actual: stringify(validatorArgs.spy) 117 | ) 118 | }.joined(separator: "\n") 119 | ) 120 | ) 121 | } 122 | } 123 | return MatcherResult(bool: false, message: validatorArgs.message) 124 | } 125 | } 126 | 127 | /// A Nimble matcher for ``Spy`` that succeeds when any of the calls to the spy are equal to the given value. 128 | /// 129 | /// - parameter value: The expected value of any of the calls to the `Spy`. 130 | /// - SeeAlso: ``beCalled(_:)-82qlg`` 131 | public func beCalled( 132 | _ value: Arguments 133 | ) -> Matcher> where Arguments: Equatable { 134 | let rawMessage = "be called with \(stringify(value))" 135 | return _beCalled(rawMessage: rawMessage) { validatorArgs in 136 | return MatcherResult( 137 | bool: validatorArgs.spy.calls.contains(value), 138 | message: validatorArgs.message 139 | ) 140 | } 141 | } 142 | 143 | /// A Nimble matcher for ``Spy`` that succeeds when the spy has been called `times` times, 144 | /// and at least one of those calls is equal to the given value. 145 | /// 146 | /// This is a shorthand for `satisfyAllOf(beCalled(times: times), beCalled(value))` 147 | /// 148 | /// - parameter value: The expected value of any of the calls to the `Spy`. 149 | /// - parameter times: The expected amount of calls the Spy should have been called. 150 | /// - SeeAlso: ``beCalled(_:times:)-6125c`` 151 | public func beCalled(_ value: Arguments, times: Int) -> Matcher> where Arguments: Equatable { 152 | let rawMessage = "be called \(times) times, at least one of them is \(stringify(value))" 153 | return _beCalled(rawMessage: rawMessage) { validatorArgs in 154 | let calls = validatorArgs.spy.calls 155 | if calls.count != times { 156 | return MatcherResult( 157 | bool: false, 158 | message: validatorArgs.message.appended( 159 | details: "but was called \(calls.count) times" 160 | ) 161 | ) 162 | } 163 | return MatcherResult( 164 | bool: calls.contains(value), 165 | message: validatorArgs.message 166 | ) 167 | } 168 | } 169 | 170 | // MARK: - Verifying the last call to the Spy 171 | 172 | /// A Nimble matcher for ``Spy`` that succeeds when the most recent call to the spy matches the given matchers. 173 | /// 174 | /// - Note: This matcher will fail if no matchers have been passed. 175 | /// - SeeAlso: ``beCalled(_:)-82qlg`` for when you want to check if any of the calls to the spy match the matcher. 176 | /// - SeeAlso: ``mostRecentlyBeCalled(_:)-91ves`` as a shorthand when Arguments is equatable, and you want to check if it's equal to some value. 177 | public func mostRecentlyBeCalled(_ matchers: Matcher...) -> Matcher> { 178 | let rawMessage = "most recently be called with \(matchers.count) matchers" 179 | return _mostRecentlyBeCalled(rawMessage: rawMessage) { validatorArgs in 180 | guard matchers.isEmpty == false else { 181 | return MatcherResult(status: .fail, message: validatorArgs.message.appended( 182 | message: "Error: No matchers were specified. " + 183 | "Use `beCalled()` to check if the spy has been called at all." 184 | )) 185 | } 186 | 187 | let matcherExpression = Expression( 188 | expression: { validatorArgs.lastCall }, 189 | location: validatorArgs.expression.location 190 | ) 191 | let results = try matchers.map { 192 | try $0.satisfies(matcherExpression) 193 | } 194 | if results.allSatisfy({ $0.toBoolean(expectation: .toMatch) }) { 195 | } 196 | return MatcherResult( 197 | bool: results.allSatisfy { $0.toBoolean(expectation: .toMatch) }, 198 | message: validatorArgs.message.appended(details: results.map { 199 | $0.message.toString( 200 | actual: stringify(validatorArgs.spy) 201 | ) 202 | }.joined(separator: "\n")) 203 | ) 204 | } 205 | } 206 | 207 | /// A Nimble matcher for ``Spy`` that succeeds when the most recent call to the spy is equal to the expected value. 208 | /// 209 | /// - SeeAlso: ``beCalled(_:)-7sn1o`` for when you want to check if any of the calls to the spy are equal to the expected value 210 | /// - SeeAlso: ``mostRecentlyBeCalled(_:)-9i9t9`` when you want to use a Matcher to check if the most recent call matches. 211 | public func mostRecentlyBeCalled( 212 | _ value: Arguments 213 | ) -> Matcher> where Arguments: Equatable { 214 | let rawMessage = "most recently be called with \(stringify(value))" 215 | return _mostRecentlyBeCalled(rawMessage: rawMessage) { validatorArgs in 216 | return MatcherResult( 217 | bool: validatorArgs.lastCall == value, 218 | message: validatorArgs.message.appended( 219 | message: "but got \(stringify(validatorArgs.lastCall))" 220 | ) 221 | ) 222 | } 223 | } 224 | 225 | extension Spy: CustomDebugStringConvertible { 226 | public var debugDescription: String { 227 | return "Spy<\(stringify(Arguments.self)), \(stringify(Returning.self))>, calls: \(stringify(calls))" 228 | } 229 | } 230 | 231 | // MARK: - Private 232 | 233 | private struct BeCalledValidatorArgs { 234 | let spy: Spy 235 | let expression: Expression> 236 | let message: ExpectationMessage 237 | 238 | init( 239 | _ spy: Spy, 240 | _ expression: Expression>, 241 | _ message: ExpectationMessage 242 | ) { 243 | self.spy = spy 244 | self.expression = expression 245 | self.message = message 246 | } 247 | } 248 | 249 | private func _beCalled( 250 | rawMessage: String, 251 | validator: @escaping (BeCalledValidatorArgs) throws -> MatcherResult 252 | ) -> Matcher> { 253 | return Matcher.define(rawMessage) { expression, message in 254 | guard let spy = try expression.evaluate() else { 255 | return MatcherResult(status: .fail, message: message.appendedBeNilHint()) 256 | } 257 | 258 | return try validator(BeCalledValidatorArgs(spy, expression, message)) 259 | } 260 | } 261 | 262 | private struct MostRecentlyBeCalledValidatorArgs { 263 | let lastCall: Arguments 264 | let spy: Spy 265 | let expression: Expression> 266 | let message: ExpectationMessage 267 | 268 | init( 269 | _ lastCall: Arguments, 270 | _ spy: Spy, 271 | _ expression: Expression>, 272 | _ message: ExpectationMessage 273 | ) { 274 | self.lastCall = lastCall 275 | self.spy = spy 276 | self.expression = expression 277 | self.message = message 278 | } 279 | } 280 | 281 | private func _mostRecentlyBeCalled( 282 | rawMessage: String, 283 | validator: @escaping ( 284 | MostRecentlyBeCalledValidatorArgs 285 | ) throws -> MatcherResult 286 | ) -> Matcher> { 287 | return _beCalled(rawMessage: rawMessage) { beCalledValidatorArgs in 288 | guard let lastCall = beCalledValidatorArgs.spy.calls.last else { 289 | return MatcherResult( 290 | status: .fail, 291 | message: beCalledValidatorArgs.message.appended( 292 | message: "but spy was never called." 293 | ) 294 | ) 295 | } 296 | return try validator( 297 | MostRecentlyBeCalledValidatorArgs( 298 | lastCall, 299 | beCalledValidatorArgs.spy, 300 | beCalledValidatorArgs.expression, 301 | beCalledValidatorArgs.message 302 | ) 303 | ) 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /Sources/Fakes/Fakes.docc/Guides/DependencyInjection.md: -------------------------------------------------------------------------------- 1 | # Dependency Injection 2 | 3 | Providing dependencies instead of reaching out to them. 4 | 5 | ## Contents 6 | 7 | **Dependency Injection** means to provide the dependencies for an object, instead 8 | of the object _a priori_ knowing how to either reach them, or how to make them 9 | itself. Dependency injection is a fundamental design pattern for enabling and 10 | improving not just testability of an object, but also the reliability of the 11 | entire system. 12 | 13 | An object can have 2 types of dependencies: Implicit and Explicit. Explicit 14 | dependencies are anything passed in or otherwise given to the object. Implicit 15 | dependencies, thus, are anything the object itself knows how to call or make. 16 | Even putting aside testability, implicit dependencies inherently make your 17 | system's dependency graph harder to read. That said, not all implicit 18 | dependencies are bad; it would be absurd to inject the `+` operator just for 19 | testability. 20 | 21 | ## Testing greetingForTimeOfDay() 22 | 23 | For example, consider a function that uses the current time in order to greet 24 | the user with one of either "Good morning", "Good afternoon", or "Good evening". 25 | Without using dependency injection, you might write the code as: 26 | 27 | ```swift 28 | func greetingForTimeOfDay() -> String { 29 | let hour = Calendar.current.component(.hour, from: Date()) 30 | switch hour { 31 | case 0..<12: return "Good morning" 32 | case 12..<18: return "Good afternoon" 33 | default: return "Good evening" 34 | } 35 | } 36 | ``` 37 | 38 | Which, sure, works, but is impossible to reliably test. The result of 39 | `greetingForTimeOfDay`, by definition, depends on the current time of 40 | day. Which means that you have three choices for testing this as-written: 41 | 42 | 1. Don't test it. 43 | 2. Write a weaker test, just checking that the output is one of the three choices: 44 | That is: `XCTAssert(["Good morning", "Good afternoon", "Good evening"].contains(greetingForTimeOfDay()))`. 45 | This is not only harder to read, but you're bundling up the three behaviors of 46 | `greetingForTimeOfDay` and making the tests less clear than asserting on the 47 | exact output. 48 | 3. Add logic to your tests, wherein you basically end up re-implementing 49 | `greetingForTimeOfDay` in your tests. 50 | That is, writing your test like: 51 | 52 | ```swift 53 | func testGreetingForTimeOfDay() { 54 | let greeting = greetingForCurrentTimeOfDay() 55 | 56 | let hour = Calendar.current.component(.hour, from: Date()) 57 | switch hour { 58 | case 0..<12: XCTAssertEqual(greeting, "Good morning") 59 | case 12..<18: XCTAssertEqual(greeting, "Good afternoon") 60 | default: XCTAssertEqual(greeting, "Good evening") 61 | } 62 | } 63 | ``` 64 | 65 | This option increases coupling, which is not what we want. The more the test 66 | knows about the internals of its subject, the more tightly coupled, the less 67 | valuable, and the harder to maintain that test will be. 68 | 69 | Additionally, in this option, we end up creating a tautology, which is a 70 | particularly useless test, as it basically ends up boiling down to "the code 71 | works because it works". The most you can say about a tautology is that at least 72 | it doesn't blow up. There are better ways to express that desire, such as using 73 | one of [Nimble's](https://github.com/quick/nimble/) 74 | [assertion-checking](https://quick.github.io/Nimble/documentation/nimble/swiftassertions), 75 | [error-checking](https://quick.github.io/Nimble/documentation/nimble/swifterrors/), 76 | or [exception-checking](https://quick.github.io/Nimble/documentation/nimble/exceptions) 77 | matchers. 78 | 79 | > Note: A Tautological Test is a test that is a mirror of the production code. 80 | In this example, this is fairly easy to identify, but they can get quite tricky 81 | as we introduce fakes. See [this post from Fabio Pereira](https://www.fabiopereira.me/blog/2010/05/27/ttdd-tautological-test-driven-development-anti-pattern/). 82 | 83 | Neither of these three options are examples of strong, reliable, easy-to-read 84 | tests. Instead, to be able to write better tests, we must improve the testability 85 | of `greetingForTimeOfDay` by injecting the dependencies. 86 | 87 | `greetingForTimeOfDay` has 2 implicit dependencies: The `Calendar` object, 88 | and the current `Date`. In this particular case, we could inject either one and 89 | be able to massively improve the testability of the function. For the sake of 90 | teaching, let's cover both. 91 | 92 | ### Injecting a DateProvider 93 | 94 | First, let's look at injecting a way to get the current `Date`, instead of 95 | making a new `Date` object that Foundation helpfully assigns to the current 96 | `Date`. We could do this in one of three ways: 97 | 98 | 1. Directly pass in a `Date` object. 99 | 2. Inject a protocol that has a single method which returns a `Date` object. 100 | 3. Inject a closure that returns a `Date` object. 101 | 102 | The first is just kicking the issue of getting the date up the stack, and thus 103 | not something we should do. 104 | The second option is a case of the 105 | [Humble Object](https://martinfowler.com/bliki/HumbleObject.html) pattern, and 106 | is an excellent way to handle this. 107 | Option 3 is only really suitable for very simple cases, and arguably not even 108 | in that case. You should know it's an option, but for the sake of consistency, 109 | you should prefer creating a protocol. 110 | 111 | With that in mind, let's introduce `DateProvider`, a single-method protocol, 112 | which is only responsible for providing a date. Alongside it, for production 113 | use, is the `CurrentDateProvider`. 114 | 115 | ```swift 116 | protocol DateProvider { 117 | func provide() -> Date 118 | } 119 | 120 | struct CurrentDateProvider: DateProvider { 121 | func provide() -> Date { Date() } 122 | } 123 | ``` 124 | 125 | > Tip: If you haven't watched the amazing WWDC 2015 talk, [Protocol Oriented 126 | Programming in Swift](https://www.youtube.com/watch?v=p3zo4ptMBiQ), be sure to 127 | take the time to do so! 128 | 129 | Now, we can refactor our `greetingForTimeOfDay` function to take in the 130 | `DateProvider`: 131 | 132 | ```swift 133 | func greetingForTimeOfDay(dateProvider: DateProvider) -> String { 134 | let hour = Calendar.current.component(.hour, from: dateProvider.provide()) 135 | switch hour { 136 | case 0..<12: return "Good morning" 137 | case 12..<18: return "Good afternoon" 138 | default: return "Good evening" 139 | } 140 | } 141 | ``` 142 | 143 | Next, we have to create a `FakeDateProvider`, which uses a `Spy` to 144 | record arguments to `provide()`, and return a pre-stubbed (that is, pre-set), 145 | date: 146 | 147 | ```swift 148 | final class FakeDateProvider: DateProvider { 149 | let provideSpy = Spy(Date(timeIntervalSince1970: 0)) 150 | func provide() { 151 | provideSpy() 152 | } 153 | } 154 | ``` 155 | 156 | Now we have all the components necessary to write our our tests for the 3 157 | potential states. Like so: 158 | 159 | ```swift 160 | final class GreetingForTimeOfDayTests: XCTestCase { 161 | var dateProvider: FakeDateProvider! 162 | override func setUp() { 163 | super.setUp() 164 | dateProvider = FakeDateProvider() 165 | } 166 | 167 | func testMorning() { 168 | // Arrange 169 | dateProvider.provideSpy.stub(Date(timeIntervalSince1970: 3600)) // Jan 1st, 1970 at ~1 am. 170 | 171 | // Act 172 | let greeting = greetingForTimeOfDay(dateProvider: dateProvider) 173 | 174 | // Assert 175 | XCTAssertEqual(greeting, "Good morning") 176 | } 177 | 178 | func testAfternoon() { 179 | // Arrange 180 | dateProvider.provideSpy.stub(Date(timeIntervalSince1970: 3600 * 12)) // Jan 1st, 1970 at ~noon am. 181 | 182 | // Act 183 | let greeting = greetingForTimeOfDay(dateProvider: dateProvider) 184 | 185 | // Assert 186 | XCTAssertEqual(greeting, "Good afternoon") 187 | } 188 | 189 | func testEvening() { 190 | // Arrange 191 | dateProvider.provideSpy.stub(Date(timeIntervalSince1970: 3600 * 18)) // Jan 1st, 1970 at ~6 pm. 192 | 193 | // Act 194 | let greeting = greetingForTimeOfDay(dateProvider: dateProvider) 195 | 196 | // Assert 197 | XCTAssertEqual(greeting, "Good evening") 198 | } 199 | } 200 | ``` 201 | 202 | > Note: Annoyed at the setup and act repetition? Check out 203 | [Quick](https://github.com/Quick/Quick/)! It provides a simple DSL enabling 204 | powerful test simplifications. 205 | 206 | This is significantly easier to understand the three behaviors that 207 | `greetingForTimeOfDay` has. However, it still requires us to carefully construct 208 | the Date objects that are then passed into the `Calendar`. So, let's look at 209 | injecting the `Calendar`. 210 | 211 | ### Injecting `Calendar` 212 | 213 | Like with providing `Date`, we have 3 options for injecting the 214 | `component(_:from:)` method. However, this case is definitely complex enough 215 | that there's really only one approach: wrapping `component(_:from:)` in a 216 | protocol. 217 | 218 | Let's do this by creating a protocol to wrap `Calendar`. For this case, we only 219 | need a single method in that protocol, but you can imagine this protocol might 220 | grow with time (or not, as per the [Interface Segregation Principle](https://en.wikipedia.org/wiki/Interface_segregation_principle)). 221 | 222 | ```swift 223 | protocol CalendarProtocol { 224 | func component( 225 | _ component: Calendar.Component, 226 | from date: Date 227 | ) -> Int 228 | } 229 | 230 | extension Calendar: CalendarProtocol {} 231 | ``` 232 | 233 | Because `component(_:from:)` is already an existing method on `Calendar`, 234 | conforming `Calendar` to `CalendarProtocol` requires no additional methods. 235 | 236 | Next, we have to inject our `CalendarProtocol` into `greetingForTimeOfDay`: 237 | 238 | ```swift 239 | func greetingForTimeOfDay(calendar: CalendarProtocol, dateProvider: DateProvider) -> String { 240 | let hour = calendar.component(.hour, from: dateProvider()) 241 | switch hour { 242 | case 0..<12: return "Good morning" 243 | case 12..<18: return "Good afternoon" 244 | default: return "Good evening" 245 | } 246 | } 247 | ``` 248 | 249 | Which, again, is a fairly straightforward thing to do. 250 | 251 | Now, let's create a Fake implementation of `CalendarProtocol`. For more details 252 | on creating fakes, please check out the tutorial . 253 | 254 | ```swift 255 | final class FakeCalendar: CalendarProtocol { 256 | let componentSpy = Spy<(component: Calendar.Component, date: Date), Int>(0) 257 | func component( 258 | _ component: Calendar.Component, 259 | from date: Date 260 | ) -> Int { 261 | componentSpy((component, date)) 262 | } 263 | } 264 | ``` 265 | 266 | Finally, we can update our tests to use `FakeCalendar`. 267 | 268 | ```swift 269 | final class GreetingForTimeOfDayTests: XCTestCase { 270 | var dateProvider: FakeDateProvider! 271 | var calendar: FakeCalendar! 272 | override func setUp() { 273 | super.setUp() 274 | dateProvider = FakeDateProvider() 275 | calendar = FakeCalendar() 276 | } 277 | 278 | func testMorning() { 279 | // Arrange 280 | calendar.componentsSpy.stub(0) 281 | 282 | // Act 283 | let greeting = greetingForTimeOfDay( 284 | calendar: calendar, 285 | dateProvider: dateProvider 286 | ) 287 | 288 | // Assert 289 | XCTAssertEqual(greeting, "Good morning") 290 | } 291 | 292 | func testAfternoon() { 293 | // Arrange 294 | calendar.componentsSpy.stub(12) 295 | 296 | // Act 297 | let greeting = greetingForTimeOfDay( 298 | calendar: calendar, 299 | dateProvider: dateProvider 300 | ) 301 | 302 | // Assert 303 | XCTAssertEqual(greeting, "Good afternoon") 304 | } 305 | 306 | func testEvening() { 307 | // Arrange 308 | calendar.componentsSpy.stub(18) 309 | 310 | // Act 311 | let greeting = greetingForTimeOfDay( 312 | calendar: calendar, 313 | dateProvider: dateProvider 314 | ) 315 | 316 | // Assert 317 | XCTAssertEqual(greeting, "Good evening") 318 | } 319 | } 320 | ``` 321 | 322 | This is significantly easier to read. By injecting our `FakeCalendar`, we were 323 | able to make `greetingForTimeOfDay` significantly easier to test, as well as 324 | drastically improving the readability and reliability of the tests. 325 | 326 | You might notice that these tests never verified that `FakeCalendar` or 327 | `dateProvider` were called. That's because it's entirely unnecessary to do so in 328 | this case. Generally, you only need to verify those as either part of 329 | scaffolding tests (tests which are only necessary while writing out the 330 | component), or when they have some kind of side effect in addition to their 331 | return value. For example, making a network call by definition has side 332 | effects (in addition to returning the data/throwing an error). So you would want 333 | to verify that the ``Spy`` for the network call has recorded a call. 334 | 335 | ### Dependency Injection for Types 336 | 337 | If `greetingForTimeOfDay` were wrapped in a type, you should inject 338 | `CalendarProtocol` and `DateProvider` as arguments to the types initializer. 339 | That way, callers of `greetingForTimeOfDay` only need the wrapping type, and not 340 | the `CalendarProtocol` and `DateProvider`. Like so: 341 | 342 | ```swift 343 | struct Greeter { 344 | let calendar: CalendarProtocol 345 | let dateProvider: DateProvider 346 | 347 | // This would normally be auto-generated, but is included here for 348 | // illustrative purposes. 349 | init(calendar: CalendarProtocol, dateProvider: DateProvider) { 350 | self.calendar = calendar 351 | self.dateProvider = dateProvider 352 | } 353 | 354 | func greetingForTimeOfDay() -> String { 355 | let hour = calendar.component(.hour, from: dateProvider()) 356 | switch hour { 357 | case 0..<12: return "Good morning" 358 | case 12..<18: return "Good afternoon" 359 | default: return "Good evening" 360 | } 361 | } 362 | } 363 | ``` 364 | 365 | > Note: By the way, did you notice the other three potential issues here? That's 366 | right, different people and cultures might define morning, afternoon, and 367 | evening differently. 368 | --------------------------------------------------------------------------------