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?
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: