iosswiftxctestsirikit

How can I compare INStartCallContactResolutionResult objects for unit testing?


I'm trying to unit test my Intent Handler class for INStartCallIntent, but I'm having trouble comparing the result objects for contact resolution.

For example, given a basic handler for 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)])
        }
    }
}

If I create a simple unit test, I'm unable to to compare the INStartCallContactResolutionResult objects:

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)
    }
}

The XCTAssertEqual fails with this message:

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; }")

So even though the two objects have identical properties, the XCTAssertEqual fails presumably because there is no equality function implemented on Apple's end.

This makes it pretty much impossible to test this function as a result. Has anyone been able to accomplish this some other way?


Solution

  • What I ended up doing here is putting the contact resolver logic in a separate helper class and wrapping the INStartCallContactResolutionResult class into a custom enum that essentially just maps it 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))])
            }
        }
    }
    

    So now I can: