Skip to content

Latest commit

 

History

History
120 lines (91 loc) · 3.2 KB

File metadata and controls

120 lines (91 loc) · 3.2 KB

AnyMockable

The AnyMockable macro allows you to semi-automatically create a mock for any protocol.

  1. Manually create a class, actor, or struct manually and add it to the desired protocol.
  2. Add a macro, methods, or properties to the created declaration. It automatically generates a mock nested class based on the implemented protocol and adds an object of this nested class.

To implement the methods, manually enable proxying of these methods for an object of the mock class. Example:

protocol ParentService {
    func checkStatus() async throws -> Bool
}

protocol IService: AnyActor, ParentService {
    var delegate: Delegate? { get async }
    var parent: ParentService { get async }

    func upload(file: Data) async throws
}

@AnyMockable
actor IServiceMock: IService {
    weak var delegate: Delegate?
    @MockAccessor // Сгенерировано макросом
    var parent: ParentService
    
    func upload(file: Data) async throws {
        try await mock.upload(file: file)
    }
    
    func checkStatus() async throws -> Bool {
        try await mock.checkStatus()
    }
    
    // Сгенерировано макросом
    internal let mock = Mock()

    internal final class Mock {
        var underlyingParent: ParentService!

        private let lock = AtomicLock()

        // MARK: - upload

        fileprivate func upload(file: Data) async throws {
            uploadFileCallsCount += 1
            uploadFileReceivedArguments.append(file)
            if let uploadFileError {
                throw uploadFileError
            }
            try await uploadFileClosure?(file)
        }
        var uploadFileCallsCount = 0
        var uploadFileReceivedArguments: [Data] = []
        var uploadFileError: Error?
        var uploadFileClosure: ((Data) async throws -> Void)?

        // MARK: - checkStatus

        fileprivate func checkStatus() async throws -> Bool {
            checkStatusCallsCount += 1
            if let checkStatusError {
                throw checkStatusError
            }
            if let checkStatusClosure {
                return try await checkStatusClosure()
            } else {
                return checkStatusReturnValue
            }
        }
        var checkStatusCallsCount = 0
        var checkStatusError: Error?
        var checkStatusClosure: (() async throws -> Bool )?
        var checkStatusReturnValue: Bool!
    }
}

// Generated by macro
extension IServiceMock: ProxyableMock { }

Using the created mock:

func test() {
    let mock = IServiceMock()
    
    ...
    
    XCTAssertEqual(mock.checkStatusCallsCount, 1)
    XCTAssertNil(mock.checkStatusError)
}

@MockAccessor

The MockAccessor auxiliary macro is automatically added to each non-optional property in the mock declaration and uses a getter and setter to proxy a similar underlying property from the internal mock class.

@MockAccessor var parent: ParentService

This macro is expanded as follows:

var parent: ParentService {
    get {
        mock.underlyingParent
    }
    set(newValue) {
        mock.underlyingParent = newValue
    }
}

Don't use the @MockAccessor macro manually. It's automatically added to each non-optional property of the mock declaration.