如何比较 INStartCallContactResolutionResult 对象以进行单元测试?

How can I compare INStartCallContactResolutionResult objects for unit testing?

我正在尝试针对 INStartCallIntent 对我的 Intent 处理程序 class 进行单元测试,但我在比较联系人解析的结果对象时遇到了问题。

例如,给定 INStartCallIntent 的基本处理程序:

import Intents

class StartCallHandler: NSObject, INStartCallIntentHandling {
        func resolveContacts(for intent: INStartCallIntent, with completion: @escaping ([INStartCallContactResolutionResult]) -> Void) {
        guard let contacts = intent.contacts, !contacts.isEmpty, let person = contacts.first else {
            completion([.needsValue()])
            return
        }
        
        guard contacts.count == 1 else {
            completion([.unsupported(forReason: .multipleContactsUnsupported)])
            return
        }

        let matchingContacts = [person] // matching logic here
        switch matchingContacts.count {
        case 2  ... Int.max:
            // We need Siri's help to ask user to pick one from the matches.
            completion([.disambiguation(with: matchingContacts)])
        case 1:
            // We have exactly one matching contact
            completion([.success(with: person)])
        default:
            completion([.unsupported(forReason: .noContactFound)])
        }
    }
}

如果我创建一个简单的单元测试,我无法比较 INStartCallContactResolutionResult 对象:

func testResolveContacts() {
    let person = INPerson(personHandle: INPersonHandle(value: nil, type: .unknown), nameComponents: nil, displayName: "Steve Jobs", image: nil, contactIdentifier: nil, customIdentifier: nil)
    let intent = INStartCallIntent(audioRoute: .unknown, destinationType: .unknown, contacts: [person], recordTypeForRedialing: .unknown, callCapability: .audioCall)
    let handler = StartCallHandler()
        
    handler.resolveContacts(for: intent) { result in
        XCTAssertEqual(result.count, 1)
        guard let firstResult = result.first else { return XCTFail() }
        
        let expectedPerson = INPerson(personHandle: INPersonHandle(value: nil, type: .unknown), nameComponents: nil, displayName: "Steve Jobs", image: nil, contactIdentifier: nil, customIdentifier: nil)
        let expectedResult = INStartCallContactResolutionResult(.success(with: expectedPerson))
        XCTAssertEqual(firstResult, expectedResult)
    }
}

XCTAssertEqual 失败并显示此消息:

XCTAssertEqual failed: ("<INStartCallContactResolutionResult: 0x600002109310> { resolutionResultCode = Success; resolvedValue = <INPerson: 0x600002c7b780> { displayName = Steve Jobs; contactIdentifier = ; nameComponents = ; image = ; customIdentifier = ; relationship = ; siriMatches = ; personHandle = <INPersonHandle: 0x600000d78960> { value = ; type = Unknown; label = ; }; }; disambiguationItems = ; itemToConfirm = ; unsupportedReason = 0; }") is not equal to ("<INStartCallContactResolutionResult: 0x6000021092c0> { resolutionResultCode = Success; resolvedValue = <INPerson: 0x600002c7b900> { displayName = Steve Jobs; contactIdentifier = ; nameComponents = ; image = ; customIdentifier = ; relationship = ; siriMatches = ; personHandle = <INPersonHandle: 0x600000d78d80> { value = ; type = Unknown; label = ; }; }; disambiguationItems = ; itemToConfirm = ; unsupportedReason = 0; }")

因此,即使这两个对象具有相同的属性,XCTAssertEqual 也可能会失败,因为 Apple 端没有实现相等函数。

这使得测试此功能变得几乎不可能。有没有人能够通过其他方式完成此操作?

我最后在这里做的是将联系人解析器逻辑放在一个单独的帮助器中 class 并将 INStartCallContactResolutionResult class 包装到一个自定义枚举中,该枚举基本上只是映射它 1:1.

public enum PersonResolverUnsupportedReason {
    case startCallContactUnsupportedReason(INStartCallContactUnsupportedReason)
}

public enum PersonResolverResult {
    case success(INPerson)
    case disambiguation([INPerson])
    case needsValue
    case unsupported
    case unsupportedWithReason(PersonResolverUnsupportedReason)
    case skip
    
    var startCallContactResolutionResult: INStartCallContactResolutionResult {
        switch self {
        case let .success(person):
            return .success(with: person)
        case let .disambiguation(persons):
            return .disambiguation(with: persons)
        case .needsValue:
            return .needsValue()
        case .unsupported:
            return .unsupported()
        case let .unsupportedWithReason(reason):
            switch reason {
            case let .startCallContactUnsupportedReason(startCallReason):
                return .unsupported(forReason: startCallReason)
            }
        case .skip:
            return .notRequired()
        }
    }
}

public protocol PersonResolverProtocol: AnyObject {
    func attemptToResolvePerson(_ person: INPerson, with: @escaping ([PersonResolverResult]) -> Void)
}

public class PersonResolver: PersonResolverProtocol {
    public func attemptToResolvePerson(_ person: INPerson, with completion: @escaping ([PersonResolverResult]) -> Void) {
        let matchingContacts = [person] // matching logic here
        switch matchingContacts.count {
        case 2...Int.max:
            completion([.disambiguation(matchingContacts.map { INPerson(...) })])
        case 1:
            guard let matchingContact = matchingContacts.first else {
                completion([.unsupportedWithReason(.startCallContactUnsupportedReason(.noContactFound))])
                break
            }
            completion([.success(INPerson(...))])
        default:
            // no contacts match
            completion([.unsupportedWithReason(.startCallContactUnsupportedReason(.noContactFound))])
        }
    }
}

现在我可以:

  • 更好地对联系解析器逻辑进行单元测试
  • 将此解析器注入 StartCallHandler
  • 如果需要,在其他意图处理程序 class 中重新使用联系解析器 class。