如何对Contacts Framework的CNContactStore进行单元测试?
单元测试fetchContacts方法的完善方案
我第一次做单元测试,要测试自己写的fetchContacts方法(该方法通过CNContactStore获取联系人),已经用Sourcery生成了CNContactStore的Mock类,目前写了部分测试代码,但不知道怎么完善,希望得到技术帮助。
相关代码如下:
待测试代码
private func fetchContacts() { var contacts: [Contact] = [] let keys: [CNKeyDescriptor] = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), CNContactPhoneNumbersKey as CNKeyDescriptor] let request = CNContactFetchRequest(keysToFetch: keys) do { try contactStore.enumerateContacts(with: request) { (contact, stop) in let name: String = CNContactFormatter.string(from: contact, style: .fullName) ?? contact.nickname contacts.append(contentsOf: contact.phoneNumbers.compactMap({ phoneNumber in let phoneNumberString: String = phoneNumber.value.stringValue return .init(name: name, phoneNumber: phoneNumberString) })) } allContacts = contacts isContactsFetched = true filterContacts() } catch { print("unable to fetch contacts") } }
Sourcery生成的Mock代码
//MARK: - enumerateContacts var enumerateContactsWithUsingBlockThrowableError: Error? var enumerateContactsWithUsingBlockCallsCount = 0 var enumerateContactsWithUsingBlockCalled: Bool { return enumerateContactsWithUsingBlockCallsCount > 0 } var enumerateContactsWithUsingBlockReceivedArguments: (fetchRequest: CNContactFetchRequest, block: (CNContact, UnsafeMutablePointer<ObjCBool>) -> Void)? var enumerateContactsWithUsingBlockReceivedInvocations: [(fetchRequest: CNContactFetchRequest, block: (CNContact, UnsafeMutablePointer<ObjCBool>) -> Void)] = [] var enumerateContactsWithUsingBlockClosure: ((CNContactFetchRequest, @escaping (CNContact, UnsafeMutablePointer<ObjCBool>) -> Void) throws -> Void)? func enumerateContacts(with fetchRequest: CNContactFetchRequest, usingBlock block: @escaping (CNContact, UnsafeMutablePointer<ObjCBool>) -> Void) throws { if let error = enumerateContactsWithUsingBlockThrowableError { throw error } enumerateContactsWithUsingBlockCallsCount += 1 enumerateContactsWithUsingBlockReceivedArguments = (fetchRequest: fetchRequest, block: block) enumerateContactsWithUsingBlockReceivedInvocations.append((fetchRequest: fetchRequest, block: block)) try enumerateContactsWithUsingBlockClosure?(fetchRequest, block) }
已编写的测试代码
it("should fetch contacts") { let contact = CNContact() let stop = UnsafeMutablePointer<ObjCBool>.allocate(capacity: 1) stop[0] = true // When viewModel.onViewDidAppear() // Then mockContactStore.enumerateContactsWithUsingBlockClosure = { (_, args) in args(contact, stop) expect(mockContactStore.enumerateContactsWithUsingBlockCallsCount).to(equal(1)) } }
完善测试的步骤和代码
1. 修正测试代码执行顺序
当前测试代码顺序存在问题:先调用viewModel.onViewDidAppear(),才设置Mock的enumerateContactsWithUsingBlockClosure,这会导致fetchContacts执行时Mock闭包还未配置,无法触发回调。必须先设置Mock闭包,再调用触发方法。
2. 构造有效的测试用CNContact数据
空的CNContact无法生成真实的姓名和手机号,需要手动创建带属性的测试联系人:
// 封装测试联系人创建方法 func createTestContact(givenName: String, familyName: String, phoneNumbers: [String]) -> CNContact { let contact = CNMutableContact() contact.givenName = givenName contact.familyName = familyName contact.phoneNumbers = phoneNumbers.map { CNLabeledValue(label: CNLabelPhoneNumberMobile, value: CNPhoneNumber(stringValue: $0)) } return contact as CNContact }
3. 完善正常场景测试
验证fetchContacts执行后,ViewModel的状态是否符合预期:
it("should fetch contacts and update viewModel state") { // Given let testContact = createTestContact(givenName: "张三", familyName: "李四", phoneNumbers: ["13800138000", "13900139000"]) let stop = UnsafeMutablePointer<ObjCBool>.allocate(capacity: 1) stop[0] = false // 允许枚举执行 // 先配置Mock闭包 mockContactStore.enumerateContactsWithUsingBlockClosure = { (_, block) in block(testContact, stop) stop[0] = true // 执行后停止枚举 } // When viewModel.onViewDidAppear() // Then expect(mockContactStore.enumerateContactsWithUsingBlockCallsCount).to(equal(1)) // 验证联系人数量:1个联系人对应2个手机号,所以应该生成2条Contact数据 expect(viewModel.allContacts.count).to(equal(2)) // 验证第一条联系人数据正确性 let firstContact = viewModel.allContacts.first expect(firstContact?.name).to(equal("张三 李四")) expect(firstContact?.phoneNumber).to(equal("13800138000")) // 验证状态标记更新 expect(viewModel.isContactsFetched).to(beTrue()) // 清理内存 stop.deallocate() }
4. 测试错误场景
验证当CNContactStore抛出错误时,fetchContacts的错误处理逻辑:
it("should handle fetch contact error") { // Given let testError = NSError(domain: "ContactError", code: -1, userInfo: [NSLocalizedDescriptionKey: "权限不足"]) mockContactStore.enumerateContactsWithUsingBlockThrowableError = testError // When viewModel.onViewDidAppear() // Then expect(mockContactStore.enumerateContactsWithUsingBlockCallsCount).to(equal(1)) // 验证错误场景下状态未更新 expect(viewModel.isContactsFetched).to(beFalse()) expect(viewModel.allContacts).to(beEmpty()) }
5. 验证请求参数正确性
额外验证fetchContacts创建的CNContactFetchRequest是否使用了正确的Key:
it("should use correct keys in CNContactFetchRequest") { // Given var receivedRequest: CNContactFetchRequest? mockContactStore.enumerateContactsWithUsingBlockClosure = { (request, block) in receivedRequest = request // 停止枚举 let stop = UnsafeMutablePointer<ObjCBool>.allocate(capacity: 1) stop[0] = true block(CNContact(), stop) stop.deallocate() } // When viewModel.onViewDidAppear() // Then expect(receivedRequest?.keysToFetch).to(contain( CNContactFormatter.descriptorForRequiredKeys(for: .fullName), CNContactPhoneNumbersKey as CNKeyDescriptor )) }
内容的提问来源于stack exchange,提问作者ferryawijayanto
相关产品推荐
相关产品推荐

