Giter Site home page Giter Site logo

mockable's Introduction

@Mockable

Mockable is a Swift macro driven testing framework that provides automatic mock implementations for your protocols. It offers an intuitive declarative syntax that simplifies the process of mocking services in unit tests. The generated mock implementations can be excluded from release builds using compile conditions.

Table of Contents

Documentation

Read Mockable's documentation for detailed installation and configuration guides as well as usage examples.

Installation

The library can be installed using Swift Package Manager.

Mockable provides two library products:

  • Mockable: Core library containing the @Mockable macro.
  • MockableTest: Testing utilities that depend on the XCTest framework (will only link with test targets).

To use the library:

  1. Add Mockable to all of your targets that contain protocols you want to mock.
  2. Add MockableTest to your test targets.

Read the installation guide of the documentation for more details on how to integrate Mockable with your project.

Configuration

Since @Mockable is a peer macro, the generated code will always be at the same scope as the protocol it is attached to.

To solve this, the macro expansion is enclosed in a pre-defined compile-time flag called MOCKING that can be leveraged to exclude generated mock implementations from release builds.

โš ๏ธ Since the MOCKING flag is not defined in your project by default, you won't be able to use mock implementations unless you configure it.

When using framework modules or a non-modular project:

Define the flag in your target's build settings for debug build configuration(s):

  1. Open your Xcode project.
  2. Go to your target's Build Settings.
  3. Find Swift Compiler - Custom Flags.
  4. Add the MOCKING flag under the debug configuration(s).

When using SPM modules or testing a package:

In your module's package manifest under the target definition, you can define the MOCKING compile-time condition if the build configuration is set to debug:

.target(
    ...
    swiftSettings: [
        .define("MOCKING", .when(configuration: .debug))
    ]
)

When using XcodeGen:

Define the flag in your XcodeGen specification:

settings:
  ...
  configs:
    debug:
      SWIFT_ACTIVE_COMPILATION_CONDITIONS: MOCKING

When using Tuist:

If you use Tuist, you can define the MOCKING flag in your target's settings under configurations.

.target(
    ...
    settings: .settings(
        configurations: [
            .debug(
                name: .debug, 
                settings: [
                    "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "$(inherited) MOCKING"
                ]
            )
        ]
    )
)

Read the configuration guide of the documentation for more details on how to setup the MOCKING flag in your project.

Usage

Example

Given a protocol annotated with the @Mockable macro:

import Mockable

@Mockable
protocol ProductService {
    var url: URL? { get set }
    func fetch(for id: UUID) async throws -> Product
    func checkout(with product: Product) throws
}

A mock implementation named MockProductService will be generated, that can be used in unit tests like:

import MockableTest

lazy var productService = MockProductService()
lazy var cartService = CartServiceImpl(productService: productService)

func testCartService() async throws {
    let mockURL = URL(string: "apple.com")
    let mockError: ProductError = .notFound
    let mockProduct = Product(name: "iPhone 15 Pro")

    given(productService)
        .fetch(for: .any).willReturn(mockProduct)
        .checkout(with: .any).willThrow(mockError)

    try await cartService.checkout(with: mockProduct, using: mockURL)

    verify(productService)
        .fetch(for: .value(mockProduct.id)).called(.atLeastOnce)
        .checkout(with: .value(mockProduct)).called(.once)
        .url(newValue: .value(mockURL)).setCalled(.once)
}

Syntax

Mockable has a declarative syntax that utilizes builders to construct given, when, and verify clauses. When constructing any of these clauses, you always follow the same syntax:

clause type(service).function builder.behavior builder

In the following example where we use the previously introduced product service:

let id = UUID()
let error: ProductError = .notFound

given(productService).fetch(for: .value(id)).willThrow(error)

We specify the following:

  • given: we want to register return values
  • (productService): we specify what mockable service we want to register return values for
  • .fetch(for: .value(id)): we want to mock the fetch(for:) method and constrain our behavior on calls with matching id parameters
  • .willThrow(error): if fetch(for:) is called with the specified parameter value, we want an error to be thrown

Parameters

Function builders have all parameters from the original requirement but encapsulate them within the Parameter<Value> type. When constructing mockable clauses, you have to specify parameter conditions for every parameter of a function. There are three available options:

  • .any: Matches every call to the specified function, disregarding the actual parameter values.
  • .value(Value): Matches to calls with an identical value in the specified parameter.
  • .matching((Value) -> Bool): Uses the provided closure to filter functions calls.

Computed properties have no parameters, but mutable properties get a (newValue:) parameter in function builders that can be used to constraint functionality on property assignment with a match condition. These newValue conditions will only effect the performOnGet, performOnSet, getCalled and setCalled clauses but will have no effect on return clauses.

Here are examples of using different parameter conditions:

// throw an error when `fetch(for:)` is called with `id`
given(productService).fetch(for: .value(id)).willThrow(error)

// print "Ouch!" if product service is called with a product named "iPhone 15 Pro"
when(productService)
  .checkout(with: .matching { $0.name == "iPhone 15 Pro" })
  .perform { print("Ouch!") }

// assert if the fetch(for:) was called exactly once regardless of what id parameter it was called with
verify(productService).fetch(for: .any).called(.once)

Given

Return values can be specified using a given(_ service:) clause. There are three return builders available:

  • willReturn(_ value:): Will store the given return value and use it to mock subsequent calls.
  • willThrow(_ error:): Will store the given error and throw it in subsequent calls. Only available for throwing functions and properties.
  • willProduce(_ producer): Will use the provided closure for mocking. The closure has the same signature as the mocked function, so for example a function that takes an integer returns a string and can throw will accept a closure of type (Int) throws -> String.

The provided return values are used up in FIFO order and the last one is always kept for any further calls. Here are examples of using return clauses:

// Throw an error for the first call and then return 'product' for every other call
given(productService)
    .fetch(for: .any).willThrow(error)
    .fetch(for: .any).willReturn(product)

// Throw an error if the id parameter ends with a 0, return a product otherwise
given(productService)
    .fetch(for: .any).willProduce { id in
        if id.uuidString.last == "0" {
            throw error
        } else {
            return product
        }
    }

When

Side effects can be added using when(_ service:) clauses. There are three kind of side effects:

  • perform(_ action): Will register an operation to perform on invocations of the mocked function.
  • performOnGet(_ action:): Available for mutable properties only, will perform the provided operation on property access.
  • performOnSet(_ action:): Available for mutable properties only, will perform the provided operation on property assignment.

Some examples of using side effects are:

// log calls to fetch(for:)
when(productService).fetch(for: .any).perform {
    print("fetch(for:) was called")
}

// log when url is accessed
when(productService).url().performOnGet {
    print("url accessed")
}

// log when url is set to nil
when(productService).url(newValue: .value(nil)).performOnSet {
    print("url set to nil")
}

Verify

You can verify invocations of your mock service using the verify(_ service:) clause. There are three kind of verifications:

  • called(_:): Asserts invocation count based on the given value.
  • getCalled(_:): Available for mutable properties only, asserts property access count.
  • setCalled(_:): Available for mutable properties only, asserts property assignment count.

Here are some example assertions:

verify(productService)
    // assert fetch(for:) was called between 1 and 5 times
    .fetch(for: .any).called(.from(1, to: 5))
    // assert checkout(with:) was called between exactly 10 times
    .checkout(with: .any).called(10)
    // assert url property was accessed at least 2 times
    .url().getCalled(.moreOrEqual(to: 2))
    // assert url property was never set to nil
    .url(newValue: .value(nil)).setCalled(.never)

If you are testing asynchronous code and cannot write sync assertions you can use the async counterparts of the above verifications:

Here are some examples of async verifications:

await verify(productService)
    // assert fetch(for:) was called between 1 and 5 times before default timeout (1 second)
    .fetch(for: .any).calledEventually(.from(1, to: 5))
    // assert checkout(with:) was called between exactly 10 times before 3 seconds
    .checkout(with: .any).calledEventually(10, before: .seconds(3))
    // assert url property was accessed at least 2 times before default timeout (1 second)
    .url().getCalledEventually(.moreOrEqual(to: 2))
    // assert url property was set to nil once
    .url(newValue: .value(nil)).setCalledEventually(.once)

Relaxed Mode

By default, you must specify a return value for all requirements; otherwise, a fatal error will be thrown. The reason for this is to aid in the discovery (and thus the verification) of every called function when writing unit tests.

However, it is common to prefer avoiding this strict default behavior in favor of a more relaxed setting, where, for example, void or optional return values do not need explicit given registration.

Use MockerPolicy (which is an option set) to implicitly mock:

  • only one kind of return value: .relaxedMockable
  • construct a custom set of policies: [.relaxedVoid, .relaxedOptional]
  • or opt for a fully relaxed mode: .relaxed.

You have two options to override the default strict behavior of the library:

  • at mock implementation level you can override the mocker policy for each individual mock implementation in the initializer:
    let relaxedMock = MockService(policy: [.relaxedOptional, .relaxedVoid])
  • at project level you can set a custom default policy to use in every scenario by changing the default property of MockerPolicy:
    MockerPolicy.default = .relaxedVoid

The .relaxedMockable policy in combination with the Mockable protocol can be used to set an implicit return value for custom (or even built in) types:

struct Car {
    var name: String
    var seats: Int
}

extension Car: Mockable {
    static var mock: Car {
        Car(name: "Mock Car", seats: 4)
    }

    // Defaults to [mock] but we can 
    // provide a custom array of cars:
    static var mocks: [Car] {
        [
            Car(name: "Mock Car 1", seats: 4),
            Car(name: "Mock Car 2", seats: 4)
        ]
    }
}

@Mockable
protocol CarService {
    func getCar() -> Car
    func getCars() -> [Car]
}

func testCarService() {
    func test() {
        let mock = MockCarService(policy: .relaxedMockable)
        // Implictly mocked without a given registration:
        let car = mock.getCar()
        let cars = mock.getCars()
    }
}

โš ๏ธ Relaxed mode will not work with generic return values as the type system is unable to locate the appropriate generic overload.

Working with non-equatable Types

Mockable uses a Matcher internally to compare parameters. By default the matcher is able to compare any custom type that conforms to Equatable (except when used in a generic function). In special cases, when you

  • have non-equatable parameter types
  • need testing specific equality logic for a custom type
  • have generic functions that are used with custom concrete types

you can register your custom types with the Matcher.register() functions. Here is how to do it:

// register an equatable type to the matcher because we use it in a generic function
Matcher.register(SomeEquatableType.self)

// register a non-equatable type to the matcher
Matcher.register(Product.self, match: { $0.name == $1.name })

// register a meta-type to the matcher
Matcher.register(HomeViewController.Type.self)

// remove all previously registered custom types
Matcher.reset()

If you see an error during tests like:

No comparator found for type "SomeType". All non-equatable types must be registered using Matcher.register(_).

Remember to add the noted type to your Matcher using the register() function.

Supported Features

  • Zero-boilerplate mock generation
  • Exclude mock implementations from production target
  • Protocols with associated types
  • Protocols with constrained associated types
  • Init requirements
  • Generic function parameters and return values
  • Generic functions with where clauses
  • Computed and mutable property requirements
  • @escaping closure parameters
  • Implicitly unwrapped optionals
  • throwing, rethrowing and async requirements
  • Custom non-equatable types

Limitations

  • Static Requirements: Static members cannot be used on protocols and are not supported.
  • Protocol Inheritance: Due to limitations of the macro system, inherited protocol requirements won't be implemented.
  • Rethrows functions: Rethrowing function requirements are always implemented with non-throwing functions.
  • Non-escaping function parameters: Non-escaping closure parameters cannot be stored and are not supported.
  • Subscripts are not supported (yet).
  • Operators are not supported (yet).

Contribution

If you encounter any issues with the project or have suggestions for improvement, please feel free to open an issue. I value your feedback and am committed to making this project as robust and user-friendly as possible.

The package manifest is set up to only contain test targets and test dependencies if an environment variable named MOCKABLE_DEV is set to true. This is done to prevent the overly zealous Swift Package Manager from downloading test dependencies and plugins, such as swift-macro-testing or SwiftLint, when someone uses Mockable as a package dependency.

To open the package with Xcode in "development mode", you need the MOCKABLE_DEV=true environment variable to be set. Use Scripts/open.sh to open the project (or copy its contents into your terminal) to be able to run tests and lint your code when contributing.

License

Mockable is made available under the MIT License. Please see the LICENSE file for more details.

mockable's People

Contributors

kolos65 avatar gaelfoppolo avatar hainayanda avatar pepicrft avatar

Stargazers

Paul Ebose avatar  avatar  avatar Aurelio avatar Ali A. Hilal avatar Yash avatar Tomoaki Yagishita avatar Maksim Zoteev avatar Wei avatar Vyacheslav Khorkov avatar Jihoon Ahn avatar Park Jonghun avatar ryohey avatar Akivaev Mark avatar Jan avatar mt.hodaka avatar Kohki Miki avatar RyuNen344 avatar jimmy8854 avatar Vincent Friedrich avatar Nick Banks avatar Yasir Tรผrk avatar Eldar Pikunov avatar  avatar Florian Fittschen avatar  avatar Erekle Meskhi avatar  avatar Daniel Cruaรฑes avatar  avatar Hyunjin avatar Jake Young avatar Iain Wilson avatar Ungjae Lee avatar Christian Stoenescu avatar Jacob Rakidzich avatar Furkan Hanci avatar Brad Bergeron avatar Cris Messias avatar Artem K. avatar Enrique Garcia avatar Matteo Matassoni avatar Andreas Koslowski avatar Ferran Abellรณ avatar Roman Trifonov avatar Wally Zhu avatar Fumiya Tanaka avatar treastrain / Tanaka Ryoga avatar zhu.bingyi avatar ะกะตั€ะณะตะน ะ“ะฐะฒั€ะธะปะพะฒ avatar  avatar tosaka avatar Sho Masegi avatar Hiroya Hinomori avatar Morten Bek Ditlevsen avatar David Catmull avatar Petr Pavlik avatar Kersten avatar  avatar Jaewon-Yun avatar Agustin avatar Doldamul avatar Barna Nรฉmeth avatar Chris avatar Sergio Padron avatar wotjd avatar Zheng Li avatar Carlos Eduardo avatar Gabriela Bezerra avatar Jonathan Pulfer avatar ไฝๆฏ… avatar ไปฃ็ ๅณๆ˜ฏๅœฐ็‹ฑ avatar insub avatar Ali ร‡olak avatar Mohamed Taher Mosbah avatar Devin Hayward avatar Wolfgang Muhsal avatar Levi Bostian avatar Christiano Gontijo avatar Lu Yao avatar Roger Prats avatar Alfayed Baksh avatar Lisnic Victor avatar Api Phoom avatar Alexander Fomin avatar James Bellamy avatar Yongjun Lee avatar Vincent avatar adamz avatar Eric Horacek avatar Johannes Plunien avatar Jonathan Yee avatar Enes Karaosman avatar Thomas Flad avatar  avatar Serj Agopian avatar vatana chhorn avatar Roman Podymov avatar Marek Foล™t avatar Joรฃo Reichert avatar

Watchers

James Bellamy avatar Barna Nรฉmeth avatar Manuel Pรฉrez avatar Ydna avatar  avatar

mockable's Issues

How to enable protocol Mockability for testing only without introducing a dependency between the main application and your library

Hi,

I think my problem has no solution. I would like to make my protocols not dependent on your library and be able to use the @Mockable macro only in a test target on my protocols. My first approach was to try using an extension like this:

@Mockable
extension MessagesManipulator {
    
}

Obviously, this doesn't work. My second approach was to try using inheritance, but as your documentation indicates, that doesn't work.

I would like to know if you have any suggestions for me to explore before I consider other approaches?

Thank you.

Protocol inheritance and composition

Hello.
Is there a way to mocking protocols that combine multiple protocols?
I try some like this, but got the error.

protocol ServiceA {
    func fooA()
}

protocol ServiceB {
    func fooB()
}

@Mockable
protocol SummaryService: ServiceA, ServiceB {}

Type 'MockSummaryService' does not conform to protocol 'ServiceA'
Type 'MockSummaryService' does not conform to protocol 'ServiceB'

Cannot have a protocol named Service

Hello,

I cant have a protocol named Service that I can apply Mockable macro, since the MockService generated will collide with an internal MockService symbol of the library.

If possible, consider renaming the internal symbol MockService to a name which is more library internal specific such that there will be smaller chances to have collisions with the clients protocols.

Thanks.

Mockable on Private Protocol Cannot Be Initialized Due to Private Initializer

In rare instances, a protocol marked with Mockable may be private and used solely within its file. If this happens, the generated mock cannot be initialized because its initializer is marked as private. While marking the protocol as fileprivate can resolve this issue, it is undesirable. Could the initializer, and generated members of the mock be restricted to an access modifier no less accessible than fileprivate?

example:
Screenshot 2024-05-29 at 9 30 58โ€ฏAM

Inlining the Mock will give me this error:

Method 'registerUser(_:password:)' must be declared fileprivate because it matches a requirement in private protocol 'RegisterViewInteractor'

Cannot find 'Type' in scope and lack of typing despite mock generation

Hey there. I've recently adopted Mockable as an alternative to Mockolo in our project as we scale up our use of modules. My first impressions are super positive, thank you for sharing this! I have just found an issue I can't reliably get around.

If I have declared a Protocol in my dynamic Framework target called APIKit, and I declare MockKeychainServiceType in my tests target called APIKitTests, I can't get any typing on it even though it actually does compile the mock correctly and I can run my tests against it.

@Mockable
public protocol KeychainServiceType {
    func store(data: Data, for key: SecureKey) throws
    func retrieve(with key: SecureKey) -> Data?
    func clear()
}

and I can see the declaration when expanding the macro, which is correct.

#if MOCKING
public final class MockKeychainServiceType: KeychainServiceType, MockService {
    private var mocker = Mocker<MockKeychainServiceType>()
    @available(*, deprecated, message: "Use given(_ service:) of Mockable instead. ")

    public func given() -> ReturnBuilder {
        .init(mocker: mocker)
    }
    @available(*, deprecated, message: "Use when(_ service:) of Mockable instead. ")

    public func when() -> ActionBuilder {
        .init(mocker: mocker)
    }
    @available(*, deprecated, message: "Use verify(_ service:) of MockableTest instead. ")

    public func verify(with assertion: @escaping MockableAssertion) -> VerifyBuilder {
        .init(mocker: mocker, assertion: assertion)
    }
    public func reset(_ scopes: Set<MockerScope> = .all) {
        mocker.reset(scopes: scopes)
    }
    public init(policy: MockerPolicy? = nil) {
        if let policy {
            mocker.policy = policy
        }
    }
    public func store(data: Data, for key: SecureKey) throws {
        let member: Member = .m1_store(data: .value(data), for: .value(key))
        try mocker.mockThrowing(member) { producer in
            let producer = try cast(producer) as (Data, SecureKey) throws -> Void
            return try producer(data, key)
        }
    }
    public func retrieve(with key: SecureKey) -> Data? {
        let member: Member = .m2_retrieve(with: .value(key))
        return mocker.mock(member) { producer in
            let producer = try cast(producer) as (SecureKey) -> Data?
            return producer(key)
        }
    }
    public func clear() {
        let member: Member = .m3_clear
        mocker.mock(member) { producer in
            let producer = try cast(producer) as () -> Void
            return producer()
        }
    }
    public enum Member: Matchable, CaseIdentifiable {
        case m1_store(data: Parameter<Data>, for: Parameter<SecureKey>)
        case m2_retrieve(with: Parameter<SecureKey>)
        case m3_clear
        public func match(_ other: Member) -> Bool {
            switch (self, other) {
            case (.m1_store(data: let leftData, for: let leftFor), .m1_store(data: let rightData, for: let rightFor)):
                return leftData.match(rightData) && leftFor.match(rightFor)
            case (.m2_retrieve(with: let leftWith), .m2_retrieve(with: let rightWith)):
                return leftWith.match(rightWith)
            case (.m3_clear, .m3_clear):
                return true
            default:
                return false
            }
        }
    }
    public struct ReturnBuilder: EffectBuilder {
        private let mocker: Mocker<MockKeychainServiceType>
        public init(mocker: Mocker<MockKeychainServiceType>) {
            self.mocker = mocker
        }
        public func store(data: Parameter<Data>, for key: Parameter<SecureKey>) -> ThrowingFunctionReturnBuilder<MockKeychainServiceType, ReturnBuilder, Void, (Data, SecureKey) throws -> Void> {
            .init(mocker, kind: .m1_store(data: data, for: key))
        }
        public func retrieve(with key: Parameter<SecureKey>) -> FunctionReturnBuilder<MockKeychainServiceType, ReturnBuilder, Data?, (SecureKey) -> Data?> {
            .init(mocker, kind: .m2_retrieve(with: key))
        }
        public func clear() -> FunctionReturnBuilder<MockKeychainServiceType, ReturnBuilder, Void, () -> Void> {
            .init(mocker, kind: .m3_clear)
        }
    }
    public struct ActionBuilder: EffectBuilder {
        private let mocker: Mocker<MockKeychainServiceType>
        public init(mocker: Mocker<MockKeychainServiceType>) {
            self.mocker = mocker
        }
        public func store(data: Parameter<Data>, for key: Parameter<SecureKey>) -> ThrowingFunctionActionBuilder<MockKeychainServiceType, ActionBuilder> {
            .init(mocker, kind: .m1_store(data: data, for: key))
        }
        public func retrieve(with key: Parameter<SecureKey>) -> FunctionActionBuilder<MockKeychainServiceType, ActionBuilder> {
            .init(mocker, kind: .m2_retrieve(with: key))
        }
        public func clear() -> FunctionActionBuilder<MockKeychainServiceType, ActionBuilder> {
            .init(mocker, kind: .m3_clear)
        }
    }
    public struct VerifyBuilder: AssertionBuilder {
        private let mocker: Mocker<MockKeychainServiceType>
        private let assertion: MockableAssertion
        public init(mocker: Mocker<MockKeychainServiceType>, assertion: @escaping MockableAssertion) {
            self.mocker = mocker
            self.assertion = assertion
        }
        public func store(data: Parameter<Data>, for key: Parameter<SecureKey>) -> ThrowingFunctionVerifyBuilder<MockKeychainServiceType, VerifyBuilder> {
            .init(mocker, kind: .m1_store(data: data, for: key), assertion: assertion)
        }
        public func retrieve(with key: Parameter<SecureKey>) -> FunctionVerifyBuilder<MockKeychainServiceType, VerifyBuilder> {
            .init(mocker, kind: .m2_retrieve(with: key), assertion: assertion)
        }
        public func clear() -> FunctionVerifyBuilder<MockKeychainServiceType, VerifyBuilder> {
            .init(mocker, kind: .m3_clear, assertion: assertion)
        }
    }
}
#endif

An example of how I use it in the test is as follows.

@testable import APIKit
import Foundation
import MockableTest
import Nimble
import Quick

class AuthenticationServiceSpec: AsyncSpec {
    override class func spec() {
        let keychainService = MockKeychainServiceType() // compiler does not know what this is
        ...
Screenshot 2024-05-01 at 07 39 56

Do you have any recommendations on how to debug this? I am going to restart my modularisation effort to see if I can source the cause. I've tried clearing derived data and reboot.

Xcode 15.3
macOS 14.4.1
M1 Macbook Pro

Async let mocks causing crashes

We have issue with async let . here is an example :

@Mockable
public protocol PersonProtocol {
    var name: String { get set }
}

public final class TestCrash {
    let person: PersonProtocol

    init(person: PersonProtocol) {
        self.person = person
    }

    private func firstFunc() async -> String  {
        return person.name
    }

    private func secondFunc() async -> String  {
        return person.name
    }

    private func thirdFunc() async -> String {
        return person.name
    }

    @MainActor
    func loadAll() async {
        async let first = firstFunc()
        async let second = secondFunc()
        async let third = thirdFunc()

        let (_,_,_) = await (first, second, third)
    }
 }
import XCTest
import MockableTest
@testable import FinanceUI

final class CrashTestCase: XCTestCase {

    func testCrash() async {
        let person = MockPersonProtocol()
        given(person).name.willReturn("")

        let sut = TestCrash(person: person)

        await sut.loadAll()
    }
}

with that we get a crash

image

[Feature request] Async verification support

Some of the mocks might be called in an async manner, and thus the verification might fail because the method is not called yet.

It would be nice if it had some async feature similar to Quick/Nimble toEventually built-in in Mockable:

// wait until routeToHome called once
await verify(mockRouter).routeToHome().eventuallyCalled(count: .once)

Optional Properties should be return nil

I have a protocol

protocol MyServicing {
var name: String?
var age: Int
}

when i use Mockable on it.

func testAge() {
given(mockMyServicing).age.willReturn(20)
}

if the implementation uses "name" , i will get the error
Fatal error: No return value found for member name.

I think it should be defaulted to nil if i don't provide it . in case of huge protocols. it would be tedious to provide all optional if they don't really relate to the testing objective.. WDYT?

Namespacing Scope enumeration

The enum name Scope may conflict with other packages. So can we put Scope into a namespace? Maybe inside a proper class/enum?

Ex: Personally I'm using Factory package which has Scope as well. So it's conflicting.

Package is using API from Combine with iOS 15 but Package.swift requires iOS 13

Minimum deployment version iOS should be iOS 15 because package is using Combine API with minimum iOS 15.
$_invocations.receive(on: queue).values

    public func verify(_ member: Member,
                       count: Count,
                       assertion: @escaping MockableAssertion,
                       timeout: TimeoutDuration,
                       file: StaticString = #file,
                       line: UInt = #line) async {
        do {
            let invocationsSequence = $_invocations.receive(on: queue).values
            try await withTimeout(after: timeout.duration) {
                for await invocations in invocationsSequence {
                    let matches = invocations.filter(member.match)
                    if count.satisfies(matches.count) {
                        break
                    } else {
                        continue
                    }
                }
            }
        } catch {
            let matches = invocations.filter(member.match)
            let message = """
            Expected \(count) invocation(s) of \(member.name) before \(timeout.duration)s, but was: \(matches.count)
            """
            assertion(count.satisfies(matches.count), message, file, line)
        }
    }

Bug: @available attribute is not added to Builder structs (v0.0.3)

@Mockable
protocol MyProtocol {
    @available(iOS 15, *)
    func foo()
}

And the generated code is below;

#if MOCKING
final class MockMyProtocol: MyProtocol, Mockable {
    private var mocker = Mocker<MockMyProtocol>()
    @available(*, deprecated, message: "Use given(_ service:) of MockableTest instead.")
    func given() -> ReturnBuilder {
        .init(mocker: mocker)
    }
    @available(*, deprecated, message: "Use when(_ service:) of MockableTest instead.")
    func when() -> ActionBuilder {
        .init(mocker: mocker)
    }
    @available(*, deprecated, message: "Use verify(_ service:) of MockableTest instead.")
    func verify(with assertion: @escaping MockableAssertion) -> VerifyBuilder {
        .init(mocker: mocker, assertion: assertion)
    }
    func reset(_ scopes: Set<MockerScope> = .all) {
        mocker.reset(scopes: scopes)
    }
    init() {
    }
    @available(iOS 15, *)
        func foo() {
        let member: Member = .m1_foo
        try! mocker.mock(member) { producer in
            let producer = try cast(producer) as () -> Void
            return producer()
        }
    }
    enum Member: Matchable, CaseIdentifiable {
        case m1_foo
        func match(_ other: Member) -> Bool {
            switch (self, other) {
            case (.m1_foo, .m1_foo):
                return true

            }
        }
    }
    struct ReturnBuilder: EffectBuilder {
        private let mocker: Mocker<MockMyProtocol>
        init(mocker: Mocker<MockMyProtocol>) {
            self.mocker = mocker
        }
        func foo() -> FunctionReturnBuilder<MockMyProtocol, ReturnBuilder, Void, () -> Void> {
            .init(mocker, kind: .m1_foo)
        }
    }
    struct ActionBuilder: EffectBuilder {
        private let mocker: Mocker<MockMyProtocol>
        init(mocker: Mocker<MockMyProtocol>) {
            self.mocker = mocker
        }
        func foo() -> FunctionActionBuilder<MockMyProtocol, ActionBuilder> {
            .init(mocker, kind: .m1_foo)
        }
    }
    struct VerifyBuilder: AssertionBuilder {
        private let mocker: Mocker<MockMyProtocol>
        private let assertion: MockableAssertion
        init(mocker: Mocker<MockMyProtocol>, assertion: @escaping MockableAssertion) {
            self.mocker = mocker
            self.assertion = assertion
        }
        func foo() -> FunctionVerifyBuilder<MockMyProtocol, VerifyBuilder> {
            .init(mocker, kind: .m1_foo, assertion: assertion)
        }
    }
}
#endif

See there is no @available attribute in ReturnBuilder, ActionBuilder and VerifyBuilder structs

Void method shouldn't need explicit given clause.

So I have this kind of routing protocol:

protocol MyViewRouting {
    func routeToSomeView()
}

I just want to verify that ViewModel is calling routeToSomeView() method once but it will throw a fatal error unless I do something like this which feels unnecessary since it's a non-throwing void method after all:

given(router) .routeToHome().willReturn()

I think in this scenario, this given shouldn't be required at all.

Getting "Invalid redeclaration" despite having different functions in protocol

Hi, I have this protocol with two methods with same name and params but different output and one of them is async

@Mockable
public protocol SendSignInCredentialsUseCase {
    func execute(
        signInCredentials: SignInCredentials,
        country: String?,
        signInMethod: AuthenticationMethod
    ) -> AnyPublisher<Session, SignInError>
    
    func execute(
        signInCredentials: SignInCredentials,
        country: String?,
        signInMethod: AuthenticationMethod
    ) async -> Result<Session, SignInError>
}

But macro is generating duplicated code at this point

public struct ActionBuilder: EffectBuilder {
        private let mocker: Mocker<MockSendSignInCredentialsUseCase>
        public init(mocker: Mocker<MockSendSignInCredentialsUseCase>) {
            self.mocker = mocker
        }
        public func execute(
                signInCredentials: Parameter<SignInCredentials>,
                country: Parameter<String?>,
                signInMethod: Parameter<AuthenticationMethod>) -> FunctionActionBuilder<MockSendSignInCredentialsUseCase, ActionBuilder> {
            .init(mocker, kind: .m2_execute(signInCredentials:
                    signInCredentials, country:
                    country, signInMethod:
                    signInMethod))
        }
        // Invalid redeclaration of 'execute(signInCredentials:country:signInMethod:)'
        public func execute(
                signInCredentials: Parameter<SignInCredentials>,
                country: Parameter<String?>,
                signInMethod: Parameter<AuthenticationMethod>) -> FunctionActionBuilder<MockSendSignInCredentialsUseCase, ActionBuilder> {
            .init(mocker, kind: .m3_execute(signInCredentials:
                    signInCredentials, country:
                    country, signInMethod:
                    signInMethod))
        }
    }
    public struct VerifyBuilder: AssertionBuilder {
        private let mocker: Mocker<MockSendSignInCredentialsUseCase>
        private let assertion: MockableAssertion
        public init(mocker: Mocker<MockSendSignInCredentialsUseCase>, assertion: @escaping MockableAssertion) {
            self.mocker = mocker
            self.assertion = assertion
        }
        public func execute(
                signInCredentials: Parameter<SignInCredentials>,
                country: Parameter<String?>,
                signInMethod: Parameter<AuthenticationMethod>) -> FunctionVerifyBuilder<MockSendSignInCredentialsUseCase, VerifyBuilder> {
            .init(mocker, kind: .m2_execute(signInCredentials:
                    signInCredentials, country:
                    country, signInMethod:
                    signInMethod), assertion: assertion)
        }
        // Invalid redeclaration of 'execute(signInCredentials:country:signInMethod:)'
        public func execute(
                signInCredentials: Parameter<SignInCredentials>,
                country: Parameter<String?>,
                signInMethod: Parameter<AuthenticationMethod>) -> FunctionVerifyBuilder<MockSendSignInCredentialsUseCase, VerifyBuilder> {
            .init(mocker, kind: .m3_execute(signInCredentials:
                    signInCredentials, country:
                    country, signInMethod:
                    signInMethod), assertion: assertion)
        }
    }

Is this something that can be avoid?

Thanks

App doesn't compile with generic destination on xcodebuild

I try to compile my app using this command
xcodebuild -scheme 'MyApp' -sdk 'iphonesimulator' -destination 'generic/platform=iOS Simulator'

but it gives me error always

protocos/MyAppService.swift:5:17: external macro implementation type 'MockableMacro.MockableMacro' could not be found for macro 'Mockable()'

It will run fine if changed the destination into -destination 'platform=iOS Simulator,name=iPhone'
but that will prevent me from using the .app file it generates in the E2E testing i have.

Use MockService in SwiftUI Previews

I guess this is not possible since the helpers are in MockableTest product not in Mockable product. But previews are located in the target not in the test target. Therefore even if we apply @Mockable macro to the Service, we cant use the helpers like given...willReturn... in Previews, correct?

Unable to use willThrow on mocked generic function

I have one blocker that might be a limitation of my implementation, so I'd like to sanity check it here. In our project, we have the following BaseAPIClient protocol.

@Mockable
public protocol BaseApiClient {
    func fetch<T: Codable>(using request: URLRequest) async throws -> T
    func cancel() async
}

public final class ApiClient: BaseApiClient {
    ...

    public func fetch<T: Codable>(using request: URLRequest) async throws -> T {
        logger.trace("API request made to \(request.detailedDescription(), privacy: .private)")
        let (data, response) = try await urlSession.data(for: request)

        if let httpResponse = response as? HTTPURLResponse {
            switch httpResponse.statusCode {
            case 200 ..< 300:
                ...
        } else {
            throw ApiError.unknownError(message: response.description)
        }
    }
}

Implementers of the class declare the BaseAPIClient type and explicitly define the `T: Codable`` return type, we get a very flexible and reusable client.

let authenticationDataResult: AuthenticationResponse = try await apiClient.fetch(using: loginRequest)

The problem is the use of given in the mock is unable to compile when I want to define a willThrow for it.

@testable import MockableGenericExample
import MockableTest
import XCTest

enum TestError: Error, Codable {
    case generic
}

final class MockableGenericExampleTests: XCTestCase {
    let baseAPIClient = MockBaseAPIClient()

    ...

    func testExample() throws {
        given(baseAPIClient)
            .fetch(using: .any)
            .willReturn("") // will compile

        given(baseAPIClient)
            .fetch(using: .any)  // won't compile due to 'Generic parameter 'T' could not be inferred'
            .willThrow(TestError.generic)

        given(baseAPIClient)
            .fetch(using: .any) // won't compile 'Type '()' cannot conform to 'Decodable'
            .willProduce { _ in
                throw TestError.generic
            }
    }
}

Have you encountered this before @Kolos65 and have any recommendations as a work around? Or is there a limitation I could help contribute a resolution towards? The alternative approach to my code is to provide an associatedType in the protocol and define that as the response without generics, which means explicit API clients that do duplicating the logic around handling the various HTTP response and error handling. I have attached an example project to help illustrate the issue. Thank you.

MockableGenericExample.zip

Opt out for MOCKING compile flag

Just a thought..

I had a situation when I needed to have a null pattern implementation for production environment of a mockable protocol which was easily created with Mock..(policy: .relaxed). However by default all mocks are under #if MOCKING compiler flag.
Would that be possible to opt out for the MOCKING flag when generating mocks? Such that the mock can be used in release configuration.
Something like this:

@Mockable(underFlag: false) // or better naming
protocol ... { }

Currently I configure MOCKING also for release config, but this makes all protocols available in release. I only needed for some protocols.
Alternative is to create the implementation for release config manually without using the Mock generated.

My test keeps crashing `EXC_BAD_ACCESS`

import Foundation
import Mockable

@Mockable
protocol TestProtocol {
    func testing(val: String) -> String
}

class TestService: TestProtocol {
    func testing(val: String = "") -> String {
        return "Testing"
    }
}


class TestViewModel: ObservableObject {
    private var services: TestProtocol
    private var value: String = ""
    
    init() {
        services = TestService()
        value = services.testing(val: "Some cool")
    }
    
    convenience init(services: TestProtocol) {
        self.init()
        self.services = services
        self.value = services.testing(val: "Whyyy")
    }
    
    func getValue() -> String {
        return value
    }
}

I have imported MockableTest into my test file, I have added MOCKING under Custom Flags for Debug and I added Mockable into my main target.

Screenshot 2024-02-05 at 09 44 30

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.