{"id":25082225,"url":"https://github.com/niksativa/sprykit","last_synced_at":"2025-12-11T20:47:26.724Z","repository":{"id":46052277,"uuid":"381353556","full_name":"NikSativa/SpryKit","owner":"NikSativa","description":"Spry is a framework that allows spying and stubbing in Apple's Swift language. Also included is a XCTAsserts for the spied objects","archived":false,"fork":false,"pushed_at":"2025-09-15T16:27:35.000Z","size":192,"stargazers_count":2,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-06T21:59:12.364Z","etag":null,"topics":["equalany","fake","generate-fake","generate-moke","helper","ios","macos","macro","macros","mock","spy","stub","stubbing","swift","swift6","test","testing","visionos","watchos"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/NikSativa.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2021-06-29T12:17:28.000Z","updated_at":"2025-08-17T12:45:01.000Z","dependencies_parsed_at":"2025-05-22T23:54:13.237Z","dependency_job_id":"57e96116-b132-438f-aca1-26e4af1dffa4","html_url":"https://github.com/NikSativa/SpryKit","commit_stats":{"total_commits":55,"total_committers":3,"mean_commits":"18.333333333333332","dds":0.07272727272727275,"last_synced_commit":"ed86f9ccf17a369ad88d925144e5e7019cd8fb3e"},"previous_names":["niksativa/spry","niksativa/nspry"],"tags_count":52,"template":false,"template_full_name":null,"purl":"pkg:github/NikSativa/SpryKit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NikSativa%2FSpryKit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NikSativa%2FSpryKit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NikSativa%2FSpryKit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NikSativa%2FSpryKit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/NikSativa","download_url":"https://codeload.github.com/NikSativa/SpryKit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NikSativa%2FSpryKit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":27669853,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-11T02:00:11.302Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["equalany","fake","generate-fake","generate-moke","helper","ios","macos","macro","macros","mock","spy","stub","stubbing","swift","swift6","test","testing","visionos","watchos"],"created_at":"2025-02-07T05:29:08.747Z","updated_at":"2025-12-11T20:47:26.715Z","avatar_url":"https://github.com/NikSativa.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SpryKit\n\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FNikSativa%2FSpryKit%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/NikSativa/SpryKit)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FNikSativa%2FSpryKit%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/NikSativa/SpryKit)\n[![NikSativa CI](https://github.com/NikSativa/SpryKit/actions/workflows/swift_macos.yml/badge.svg)](https://github.com/NikSativa/SpryKit/actions/workflows/swift_macos.yml)\n[![License](https://img.shields.io/github/license/Iterable/swift-sdk)](https://opensource.org/licenses/MIT)\n\nSpryKit is a powerful Swift testing framework that provides spying and stubbing capabilities, making it easier to write clean and maintainable unit tests. It's designed to help you test classes in isolation by verifying method calls and controlling return values.\n\n\u003e [!IMPORTANT]\n\u003e Thread-safe: perfect for multi-threaded test environments.\n\n## Best Practices\n\n1. **Prefer Macros (Swift 6.0+)**  \n   Use macros whenever possible—they reduce boilerplate, eliminate implementation errors, and keep test doubles concise.\n\n2. **Reset Test Doubles Between Tests**  \n   Always call `resetCallsAndStubs()` in `tearDown()` to ensure each test starts from a clean state.\n\n3. **Use Argument Captors for Detailed Validation**  \n   When argument values are complex or dynamic, `ArgumentCaptor` allows you to assert their content precisely.\n\n4. **Take Advantage of Rich Assertion Messages**  \n   SpryKit’s assertions provide detailed output on failure, making tests easier to debug and maintain.\n\n5. **Stub Error Cases Intentionally**  \n   Ensure you test both success and failure scenarios by stubbing functions to throw errors or return unexpected values.\n\n6. **Fake naming convention**  \n   `Fake` classes (named with the `Fake` prefix, e.g., `FakeUserService`) should be used for all test doubles—both manual and macro-generated—in your tests. This naming convention makes their purpose obvious and keeps your tests clear.\n\n## Motivation\n\nWhen writing unit tests, it's considered best practice to isolate and test the behavior of a single class—referred to as the Subject Under Test (SUT). Swift makes this difficult when you need to verify interactions with dependencies, such as whether methods are called, what arguments they receive, and what values they return.\n\n**SpryKit** addresses this challenge by allowing you to create spy objects that record function calls and arguments, and stub objects that return specific values or perform custom behavior. This makes it possible to write focused, deterministic tests that verify the behavior of the SUT in isolation.\n\n## Why SpryKit?\n\nTraditional mocking and stubbing in Swift can be verbose and error-prone, especially when verifying method calls and handling dynamic return values. **SpryKit** streamlines this process with a unified API that:\n\n- Minimizes boilerplate through macro generation\n- Provides expressive, readable test assertions\n- Ensures thread safety and stability for concurrent tests\n- Offers fine-grained argument control with validation and capturing\n\n## Features\n\n- 🎯 **Spying**: Record and verify method calls and their arguments\n- 🎭 **Stubbing**: Control method return values for testing different scenarios\n- 🚀 **Macro Support**: Reduce boilerplate with Swift 6.0+ macros\n- 🔒 **Thread Safety**: Built-in support for multi-threaded environments\n- 📱 **Cross-Platform**: Support for iOS, macOS, tvOS, watchOS, and visionOS\n- 🧪 **Rich Assertions**: Comprehensive set of XCTest assertions\n- 🔍 **Argument Capturing**: Capture and inspect method arguments\n- 🎨 **Image Testing**: Built-in support for image comparison testing\n\n__Table of Contents__\n\n### 📘 Overview\n* [Best Practices](#best-practices)\n* [Motivation](#motivation)\n* [Why SpryKit](#why-sprykit)\n* [Features](#features)\n* [Installation](#installation)\n* [Quick Start](#quick-start)\n\n### 🛠️ Core Concepts\n* [Spying](#spying)\n* [Stubbing](#stubbing)\n* [Argument Capturing](#argument-capturing)\n* [Spryable](#spryable)\n  * [Spryable + Macro](#spryable--macro)\n  * [Spryable + manually](#spryable--manually)\n* [Stubbable](#stubbable)\n* [Spyable](#spyable)\n* [XCTAsserts](#xctasserts)\n* [SpryEquatable](#spryequatable)\n* [Argument](#argument)\n* [ArgumentCaptor](#argumentcaptor)\n* [MacroAvailable](#macroavailable)\n\n### 🧪 Advanced Testing\n* [XCTAssertHaveReceived / XCTAssertHaveNotReceived](#xctasserthavereceived--xctasserthavenotreceived)\n* [XCTAssertEqualAny / XCTAssertNotEqualAny](#xctassertequalany--xctassertnotequalany)\n* [XCTAssertThrowsAssertion](#xctassertthrowsassertion)\n* [XCTAssertThrowsError / XCTAssertNoThrowError](#xctassertthrowserror--xctassertnothrowerror)\n* [XCTAssertEqualError / XCTAssertNotEqualError](#xctassertequalerror--xctassertnotequalerror)\n* [XCTAssertEqualImage / XCTAssertNotEqualImage](#xctassertequalimage--xctassertnotequalimage)\n\n### 🎨 Visuals\n* [Visual Overview](#visual-overview)\n* [How It Works in Tests](#how-it-works-in-tests)\n\n### 📚 Meta\n* [Requirements](#requirements)\n* [Contributing](#contributing)\n* [License](#license)\n\n## Installation\n\n### Swift Package Manager\n\nAdd the following to your `Package.swift`:\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/NikSativa/SpryKit.git\", from: \"1.0.0\")\n]\n```\n\n## Quick Start\n\n### 1. Create a Protocol or Class to Test\n\n```swift\nprotocol UserService {\n    func fetchUser(id: String) -\u003e User\n    var currentUser: User? { get set }\n}\n```\n\n### 2. Create a Fake Implementation\n\n#### Using Swift 6.0+ Macros (Recommended)\n\n\u003e [!NOTE]\n\u003e Macros reduce boilerplate by generating protocol conformance and necessary stubbing/spying code for you. All you need to do is annotate your class or properties/functions.\n\n```swift\n@Spryable\nfinal class FakeUserService: UserService {\n    @SpryableVar\n    var currentUser: User?\n    \n    @SpryableFunc\n    func fetchUser(id: String) -\u003e User\n}\n```\n\n#### Manual Implementation\n\nIf you're unable to use macros (e.g., due to Swift version constraints), you can implement `Spryable` manually as shown:\n\n```swift\nfinal class FakeUserService: UserService, Spryable {\n    enum Function: String, StringRepresentable {\n        case currentUser\n        case fetchUser = \"fetchUser(id:)\"\n    }\n    \n    var currentUser: User? {\n        get { return stubbedValue() }\n        set { recordCall(arguments: newValue) }\n    }\n    \n    func fetchUser(id: String) -\u003e User {\n        return spryify(arguments: id)\n    }\n}\n```\n\n### 3. Write Tests\n\n```swift\nclass UserViewModelTests: XCTestCase {\n    var sut: UserViewModel!\n    var fakeUserService: FakeUserService!\n    \n    override func setUp() {\n        super.setUp()\n        fakeUserService = FakeUserService()\n        sut = UserViewModel(userService: fakeUserService)\n    }\n    \n    override func tearDown() {\n        fakeUserService.resetCallsAndStubs()\n        super.tearDown()\n    }\n    \n    func test_fetchUser_success() {\n        // Given\n        let expectedUser = User(id: \"1\", name: \"John\")\n        fakeUserService.stub(.fetchUser).with(\"1\").andReturn(expectedUser)\n        \n        // When\n        sut.fetchUser(id: \"1\")\n        \n        // Then\n        XCTAssertHaveReceived(fakeUserService, .fetchUser)\n        XCTAssertEqual(sut.currentUser, expectedUser)\n    }\n}\n```\n\n## Core Concepts\n\n### Spying\n\nSpying allows you to verify that methods were called with the correct arguments:\n\n```swift\n// Verify a method was called\nXCTAssertHaveReceived(fakeService, .doSomething)\n\n// Verify with specific arguments\nXCTAssertHaveReceived(fakeService, .doSomething, with: \"expected argument\")\n\n// Verify call count\nXCTAssertHaveReceived(fakeService, .doSomething, times: .exactly(2))\n```\n\n### Stubbing\n\nStubbing lets you control what methods return:\n\n```swift\n// Simple return value\nfakeService.stub(.doSomething).andReturn(\"test value\")\n\n// Conditional return based on arguments\nfakeService.stub(.doSomething)\n    .with(\"specific arg\")\n    .andReturn(\"special value\")\n\n// Custom implementation\nfakeService.stub(.doSomething).andDo { arguments in\n    let arg = arguments[0] as! String\n    return arg.uppercased()\n}\n```\n\n### Argument Capturing\n\nCapture and inspect arguments passed to methods:\n\n```swift\nlet captor = Argument.captor()\nfakeService.stub(.doSomething).with(Argument.anything, captor).andReturn(\"value\")\n\n// Later in the test\nlet capturedArg = captor.getValue(as: String.self)\nXCTAssertEqual(capturedArg, \"expected value\")\n```\n\n## Advanced Features\n\n### Custom Argument Validation\n\n```swift\nlet customValidation = Argument.pass { actualArgument -\u003e Bool in\n    guard let string = actualArgument as? String else { return false }\n    return string.hasPrefix(\"test\")\n}\n\nfakeService.stub(.doSomething)\n    .with(customValidation)\n    .andReturn(\"validated\")\n```\n\n### Testing Errors\n\n```swift\n// Test throwing functions\nXCTAssertThrowsError(try sut.riskyOperation())\n\n// Test specific errors\nXCTAssertEqualError(try sut.riskyOperation(), expectedError)\n```\n\n### Image Testing\n\n```swift\nXCTAssertEqualImage(actualImage, expectedImage)\n```\n\n## Spryable\n\nConform to both Stubbable and Spyable at the same time! For information about [Stubbable](#stubbable) and [Spyable](#spyable) see their respective sections below.\n\n__Abilities__\n\n* Conform to `Spyable` and `Stubbable` at the same time.\n* Reset calls and stubs at the same time with `resetCallsAndStubs()`\n* Easy to implement\n    * Create an object that conforms to `Spryable`\n    * In every function (the ones that should be stubbed and spied) return the result of `spryify()` passing in all arguments (if any)\n        * also works for special functions like `subscript`\n    * In every property (the ones that should be stubbed and spied) return the result of `stubbedValue()` in the `get {}` and use `recordCall()` in the `set {}`\n\nLet’s look at an example\n\n```swift\n// A real implementation can be a protocol\nprotocol StringService: AnyObject {\n    var readonlyProperty: String { get }\n    var readwriteProperty: String { set get }\n    func doThings()\n    func giveMeAString(arg1: Bool, arg2: String) -\u003e String\n    static func giveMeAString(arg1: Bool, arg2: String) -\u003e String\n}\n\n// A real implementation can also be a class\nclass RealStringService: StringService {\n    var readonlyProperty: String {\n        return \"\"\n    }\n\n    var readwriteProperty: String = \"\"\n\n    func doThings() {\n        // do real things\n    }\n\n    func giveMeAString(arg1: Bool, arg2: String) -\u003e String {\n        // do real things\n        return \"\"\n    }\n\n    class func giveMeAString(arg1: Bool, arg2: String) -\u003e String {\n        // do real things\n        return \"\"\n    }\n}\n```\n\n### Spryable + Macro\n\n\u003e [!WARNING]\n\u003e **Available only for Swift 6.0 and higher.**\n\n\u003e [!TIP]\n\u003e [MacroAvailable](#MacroAvailable) - how to handle breaking API changes.\n\n- *Spryable* macro generates Spryable conformance for a class.\n- *SpryableFunc* macro generates body for function with correct name and arguments.\n- *SpryableVar* macro generates body for property with correct name and accessors.\n\n```swift\n@Spryable\nfinal class FakeStringService: StringService {\n    @SpryableVar\n    var readonlyProperty: String\n\n    @SpryableVar(.set)\n    var readwriteProperty: String\n\n    @SpryableFunc\n    func doThings()\n\n    @SpryableFunc\n    func giveMeAString(arg1: Bool, arg2: String) -\u003e String\n\n    @SpryableFunc\n    static func giveMeAString(arg1: Bool, arg2: String) -\u003e String\n}\n```\n\n### Spryable + manually\n\n```swift\n// The **Fake** Class: Always use the \"Fake\" prefix to indicate a test double (e.g., `FakeUserService`).\n// If the Fake is a subclass, remember that `override` is required for each function and property.\nfinal class FakeUserService: UserService, Spryable {\n    enum ClassFunction: String, StringRepresentable {  \n        case giveMeAStringWithArg1_Arg2 = \"giveMeAString(arg1:arg2:)\"\n    }\n    enum Function: String, StringRepresentable { \n        case readonlyProperty\n        case readwriteProperty\n        case doThings = \"doThings()\"\n        case giveMeAStringWithArg1_Arg2 = \"giveMeAString(arg1:arg2:)\"\n    }\n\n    var readonlyProperty: String {\n        return stubbedValue()\n    }\n\n    var readwriteProperty: String {\n        set {\n            recordCall(arguments: newValue)\n        }\n        get {\n            return stubbedValue()\n        }\n    }\n\n    func doThings() {\n        return spryify() \n    }\n\n    func giveMeAString(arg1: Bool, arg2: String) -\u003e String {\n        return spryify(arguments: arg1, arg2) \n    }\n\n    static func giveMeAString(arg1: Bool, arg2: String) -\u003e String {\n        return spryify(arguments: arg1, arg2) \n    }\n}\n```\n\n## Stubbable\n\n_Spryable conforms to Stubbable._\n\n__Abilities__\n\n* Stub a return value for a function on an instance of a class or the class itself using `.andReturn()`\n* Stub the implementation for a function on an instance of a class or the class itself using `.andDo()`\n    * `.andDo()` takes in a closure that passes in an `Array` containing the parameters and should return the stubbed value\n* Specify stubs that only get used if the right arguments are passed in using `.with()` (see [Argument Enum](#argument-enum) for alternate specifications)\n* Rich `fatalError()` messages that include a detailed list of all stubbed functions when no stub is found (or the arguments received didn't pass validation)\n* Reset stubs with `resetStubs()`\n\n```swift\n// will always return `\"stubbed value\"`\nfakeStringService.stub(.hereAreTwoStrings).andReturn(\"stubbed value\")\n\n// defaults to return Void()\nfakeStringService.stub(.hereAreTwoStrings).andReturn()\n\n// specifying all arguments (will only return `true` if the arguments passed in match \"first string\" and \"second string\")\nfakeStringService.stub(.hereAreTwoStrings).with(\"first string\", \"second string\").andReturn(true)\n\n// using the Argument enum (will only return `true` if the second argument is \"only this string matters\")\nfakeStringService.stub(.hereAreTwoStrings).with(Argument.anything, \"only this string matters\").andReturn(true)\n\n// using custom validation\nlet customArgumentValidation = Argument.pass({ actualArgument -\u003e Bool in\n    let passesCustomValidation = // ...\n    return passesCustomValidation\n})\nfakeStringService.stub(.hereAreTwoStrings).with(Argument.anything, customArgumentValidation).andReturn(\"stubbed value\")\n\n// using argument captor\nlet captor = Argument.captor()\nfakeStringService.stub(.hereAreTwoStrings).with(Argument.nonNil, captor).andReturn(\"stubbed value\")\ncaptor.getValue(as: String.self) // gets the second argument the first time this function was called where the first argument was also non-nil.\ncaptor.getValue(at: 1, as: String.self) // gets the second argument the second time this function was called where the first argument was also non-nil.\n\n// using `andDo()` - Also has the ability to specify the arguments!\nfakeStringService.stub(.iHaveACompletionClosure).with(\"correct string\", Argument.anything).andDo({ arguments in\n    // get the passed in argument\n    let completionClosure = arguments[0] as! () -\u003e Void\n\n    // use the argument\n    completionClosure()\n\n    // return an appropriate value\n    return Void() // \u003c-- will be returned by the stub\n})\n\n// can stub class functions as well\nFakeStringService.stub(.imAClassFunction).andReturn(Void())\n\n// do not forget to reset class stubs (since Class objects are essentially singletons)\nFakeStringService.resetStubs()\n```\n\n## Spyable\n\n_Spryable conforms to Spyable._\n\n__Abilities__\n\n* Test whether a function was called or a property was set on an instance of a class or the class itself\n* Specify the arguments that should have been received along with the call (see [Argument Enum](#argument-enum) for alternate specifications)\n* Rich Failure messages that include a detailed list of called functions and arguments\n* Reset calls with `resetCalls()`\n\n__The Result__\n\n```swift\n// the result\nlet result = spyable.didCall(.functionName)\n\n// was the function called on the fake?\nresult.success\n\n// what was called on the fake?\nresult.recordedCallsDescription\n```\n\n__How to Use__\n\n```swift\n// passes if the function was called\nfake.didCall(.functionName).success\n\n// passes if the function was called a number of times\nfake.didCall(.functionName, countSpecifier: .exactly(1)).success\n\n// passes if the function was called at least a number of times\nfake.didCall(.functionName, countSpecifier: .atLeast(1)).success\n\n// passes if the function was called at most a number of times\nfake.didCall(.functionName, countSpecifier: .atMost(1)).success\n\n// passes if the function was called with equivalent arguments\nfake.didCall(.functionName, withArguments: [\"firstArg\", \"secondArg\"]).success\n\n// passes if the function was called with arguments that pass the specified options\nfake.didCall(.functionName, withArguments: [Argument.nonNil, Argument.anything, \"thirdArg\"]).success\n\n// passes if the function was called with an argument that passes the custom validation\nlet customArgumentValidation = Argument.pass({ argument -\u003e Bool in\n    let passesCustomValidation = // ...\n    return passesCustomValidation\n})\nfake.didCall(.functionName, withArguments: [customArgumentValidation]).success\n\n// passes if the function was called with equivalent arguments a number of times\nfake.didCall(.functionName, withArguments: [\"firstArg\", \"secondArg\"], countSpecifier: .exactly(1)).success\n\n// passes if the property was set to the right value\nfake.didCall(.propertyName, with: \"value\").success\n\n// passes if the class function was called\nFake.didCall(.functionName).success\n```\n\n## XCTAsserts\n\nSpryKit provides a set of `XCTAssert` functions to make testing with SpryKit easier.\n\n### XCTAssertHaveReceived / XCTAssertHaveNotReceived\n\nHave Received Matcher is made to be used with XCTest.\n\n```swift\n// passes if the function was called\nXCTAssertHaveReceived(fake, .functionName)\n\n// passes if the function was called a number of times\nXCTAssertHaveReceived(fake, .functionName, countSpecifier: .exactly(1))\n\n// passes if the function was called at least a number of times\nXCTAssertHaveReceived(fake, .functionName, countSpecifier: .atLeast(2))\n\n// passes if the function was called at most a number of times\nXCTAssertHaveReceived(fake, .functionName, countSpecifier: .atMost(1))\n\n// passes if the function was called with equivalent arguments\nXCTAssertHaveReceived(fake, .functionName, with: \"firstArg\", \"secondArg\")\n\n// passes if the function was called with arguments that pass the specified options\nXCTAssertHaveReceived(fake, .functionName, with: Argument.nonNil, Argument.anything, \"thirdArg\")\n\n// passes if the function was called with an argument that passes the custom validation\nlet customArgumentValidation = Argument.validator({ argument -\u003e Bool in\n    let passesCustomValidation = // ...\n    return passesCustomValidation\n})\nXCTAssertHaveReceived(fake, .functionName, with: customArgumentValidation)\n\n// passes if the function was called with equivalent arguments a number of times\nXCTAssertHaveReceived(fake, .functionName, with: \"firstArg\", \"secondArg\", countSpecifier: .exactly(1))\n\n// passes if the property was set to the specified value\nXCTAssertHaveReceived(fake, .propertyName, with: \"value\")\n\n// passes if the class function was called\nXCTAssertHaveReceived(Fake.self, .functionName)\n\n// passes if the class property was set\nXCTAssertHaveReceived(Fake.self, .propertyName)\n\n// do not forget to reset calls on class objects (since Class objects are essentially singletons)\nFake.resetCallsAndStubs()\n```\n\n### XCTAssertEqualAny / XCTAssertNotEqualAny\n\nFunction that compares two values of any type. This is useful when you need to compare two instances of a class or struct even if they do not conform to the `Equatable` protocol.\n\n```swift\nstruct User {\n    let name: String\n    let age: Int\n}\nXCTAssertEqualAny(User(name: \"John\", age: 30), User(name: \"John\", age: 30))\nXCTAssertNotEqualAny(User(name: \"Bob\", age: 20), User(name: \"John\", age: 30))\n```\n\n### XCTAssertThrowsAssertion\n\nFunction that checks if the block throws an assertion.\n\n```swift\nXCTAssertThrowsAssertion {\n    assertionFailure(\"should catch this assertion failure\")\n}\n```\n\n### XCTAssertThrowsError / XCTAssertNoThrowError\n\nFunction that checks if the block throws an error.\n\n```swift\nprivate func throwError() throws {\n    throw XCTAssertThrowsErrorTests.Error.one\n}\n\nXCTAssertThrowsError(Error.one) {\n    try throwError()\n}\n\nprivate func notThrowError() throws {\n    // nothing\n}\nXCTAssertNoThrowError(try notThrowError())\n```\n\n### XCTAssertEqualError / XCTAssertNotEqualError\n\nFunction that compares two errors.\n```swift\nXCTAssertEqualError(Error.one, Error.one)\nXCTAssertNotEqualError(Error.one, Error.two)\n```\n\n### XCTAssertEqualImage / XCTAssertNotEqualImage\n\nFunction that compares two images by their data representation even if they are not the same type.\n\n\u003e [!TIP]\n\u003e Use mocked images by `UIImage.spry.testImage`\n\n```swift\nXCTAssertEqualImage(Image.spry.testImage, Image.spry.testImage)\nXCTAssertNotEqualImage(Image.spry.testImage, Image.spry.testImage2)\n```\n\n## SpryEquatable\n\nSpryKit uses the `SpryEquatable` protocol to override comparisons in your test classes at your own risk. This is useful when you need to compare two instances of a class or struct even if they do not conform to the `Equatable` protocol, or when you need to skip some properties in the comparison (e.g., closures). Make types conform to `SpryEquatable` only when you need something very specific; otherwise, use the `Equatable` protocol or `XCTAssertEqualAny`.\n\n```swift\n// custom type\nextension Person: SpryEquatable {\n    public static func == (lhs: Person, rhs: Person) -\u003e Bool {\n        return lhs.name == rhs.name\n            \u0026\u0026 lhs.age == rhs.age\n    }\n}\n```\n\n## Argument\n\nUse when the exact comparison of an argument using the `Equatable` protocol is not desired, needed, or possible.\n\n* `case anything`\n    * Used to indicate that absolutely anything passed in will be sufficient.\n* `case nonNil`\n    * Used to indicate that anything non-nil passed in will be sufficient.\n* `case nil`\n    * Used to indicate that only nil passed in will be sufficient.\n* `case validator`\n    * Used to provide custom validation for a specific argument.\n    * The associated value is a closure which takes in the argument and returns a bool to indicate whether or not it passed validation.\n* `func captor`\n    * Used to create a new [ArgumentCaptor](#argumentcaptor)\n    * An argument captor is used to capture arguments as the function is called so that they can be accessed at a later point.\n* `func isType\u003cT\u003e`\n    * Type is exactly the type passed in match this qualification (subtypes do NOT qualify).\n* `func instanceOf\u003cT\u003e`\n    * Only objects whose type is exactly the type passed in match this qualification (subtypes do NOT qualify).\n\n## ArgumentCaptor\n\nArgumentCaptor is used to capture a specific argument when the stubbed function is called. Afterward the captor can serve up the captured argument for custom argument checking. An ArgumentCaptor will capture the specified argument every time the stubbed function is called.\nCaptured arguments are stored in chronological order for each function call. When getting an argument you can specify which argument to get (defaults to the first time the function was called)\nWhen getting a captured argument the type must be specified. If the argument can not be cast as the type given then a `fatalError()` will occur.\n\n```swift\nlet captor = Argument.captor()\nfakeStringService.stub(.hereAreTwoStrings).with(Argument.anything, captor).andReturn(\"stubbed value\")\n\n_ = fakeStringService.hereAreTwoStrings(string1: \"first arg first call\", string2: \"second arg first call\")\n_ = fakeStringService.hereAreTwoStrings(string1: \"first arg second call\", string2: \"second arg second call\")\n\nlet secondArgFromFirstCall = captor.getValue(as: String.self) // `at:` defaults to `0` or first call\nlet secondArgFromSecondCall = captor.getValue(at: 1, as: String.self)\n// or\nlet secondArgFromFirstCall: String = captor[0]\nlet secondArgFromSecondCall: String = captor[1]\n```\n\n## MacroAvailable\n\nAll the ideas described in the following apply to all packages that depend on SpryKit, not only macros.\n\nIn order to handle breaking API changes, clients can wrap uses of such APIs in conditional compilation clauses that check MacroAvailable.\nUsing Swift 6.0+ macros, SpryKit automatically creates all the plumbing required to spy on function calls and stub return values—eliminating manual boilerplate and reducing the risk of human error.\n\n```swift\n#if canImport(SpryMacroAvailable)\n// code to support @Spryable\n#else\n// code for SpryKit without Macro\n#endif\n```\n\n## Visual Overview\n\nThe diagram below provides a high-level overview of how SpryKit fits into your test suite.\n\n```mermaid\ngraph TD\n  A[Test Class] --\u003e|uses| B[Fake Service]\n  B --\u003e|conforms to| C[Spryable]\n  C --\u003e D[Spyable]\n  C --\u003e E[Stubbable]\n  B --\u003e|macros generate| F[SpryableFunc / SpryableVar]\n  D --\u003e|records| G[Function Calls \u0026 Arguments]\n  E --\u003e|controls| H[Return Values \u0026 Custom Behavior]\n  G --\u003e I[XCTAssertHaveReceived]\n  H --\u003e J[Stub Configurations]\n```\n\nThis flow illustrates how your test classes interact with fake services, and how SpryKit helps you monitor and control behaviors for precise and effective testing.\n\n\n## How It Works in Tests\n\nThe diagram below illustrates how a fake service interacts with a subject under test (SUT) inside a typical unit test, and how the dependency protocol plays a role.\n```mermaid\nsequenceDiagram\n  participant T as Test Case\n  participant S as Subject Under Test (SUT)\n  participant F as Fake Service (Spryable)\n  participant P as Dependency Protocol\n\n  T-\u003e\u003eP: Define protocol (e.g., UserService)\n  F-\u003e\u003eP: Conform to protocol\n  T-\u003e\u003eF: Configure stubs (e.g., .andReturn)\n  T-\u003e\u003eS: Inject Fake Service\n  S-\u003e\u003eF: Call (e.g., fetchUser(id:))\n  F--\u003e\u003eS: Return stubbed User\n  T-\u003e\u003eF: Verify call using XCTAssertHaveReceived\n```\n\nThis sequence highlights how SpryKit helps you define a contract via a protocol, provide a fake that conforms to it, and test the SUT's interaction with that dependency in a controlled way.\n\n## Requirements\n\n- iOS 13.0+\n- macOS 11.0+\n- macCatalyst 13.0+\n- tvOS 13.0+\n- watchOS 6.0+\n- visionOS 1.0+\n- Swift 5.9+\n\n## Contributing\n\nIf you have an idea that can make SpryKit better, please don't hesitate to submit a pull request!\n\n## License\n\nSpryKit is available under the MIT license. See the LICENSE file for more info.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fniksativa%2Fsprykit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fniksativa%2Fsprykit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fniksativa%2Fsprykit/lists"}