你把客户联系人导出成 .vcf,分享面板能打开,导入后却只剩一个号码,姓名中的分号也变了位置。这类问题不靠截图验收。在云端 Mac 上先把“生成数据→重新解析→逐字段比较”跑通,再去 iOS 检查分享和导入,能把格式错误与界面问题分开。
先定义需要保留什么
本文验证应用内存模型到 vCard 的转换,不读取真实通讯录。用合成联系人覆盖中文、重音字符、多个号码和自定义标签,避免把个人数据带进构建日志。
第一版契约可以很小:姓、名必须原样返回;电话号码保留输入字符串;自定义标签与号码的对应关系不能改变;空字段不能凭空获得业务默认值。号码规范化应由独立逻辑负责,不要在导出时顺手删掉前导加号。
文件能解析,只证明语法可接受;字段回读一致,才证明这条转换路径没有丢掉你关心的数据。
暂时不加入头像、备注和群组。它们有不同的容量、访问或兼容性边界,应该单独扩展样本,而不是让一次失败同时牵涉所有字段。
用系统序列化器建立基准
在已安装 Xcode 命令行工具的云端 Mac 上,把下面内容保存为 vcard-check.swift。它仅使用 Foundation 和 Contacts,不调用通讯录查询接口,也不需要申请通讯录权限。
import Foundation
import Contacts
struct CheckFailure: Error {
let field: String
}
func require(_ condition: Bool, _ field: String) throws {
if !condition {
throw CheckFailure(field: field)
}
}
func phones(_ contact: CNContact) -> [String: String] {
var result: [String: String] = [:]
for item in contact.phoneNumbers {
result[item.label ?? ""] = item.value.stringValue
}
return result
}
do {
let source = CNMutableContact()
source.familyName = "林;测试"
source.givenName = "Zoë,研发"
source.phoneNumbers = [
CNLabeledValue(
label: "build-primary",
value: CNPhoneNumber(stringValue: "+12025550101")
),
CNLabeledValue(
label: "build-secondary",
value: CNPhoneNumber(stringValue: "+12025550102")
)
]
let data = try CNContactVCardSerialization.data(with: [source])
try data.write(to: URL(fileURLWithPath: "contact.vcf"),
options: .atomic)
let contacts = try CNContactVCardSerialization.contacts(with: data)
try require(contacts.count == 1, "contact-count")
let restored = contacts[0]
try require(restored.familyName == source.familyName, "family-name")
try require(restored.givenName == source.givenName, "given-name")
try require(restored.phoneNumbers.count == 2, "phone-count")
try require(phones(restored) == phones(source), "phone-values")
} catch {
FileHandle.standardError.write(Data("vCard check failed\n".utf8))
exit(1)
}
执行后检查进程退出码,而不是搜索“成功”字样:
mkdir -p artifacts/vcard
cd artifacts/vcard
xcrun swift ../../vcard-check.swift
示例假设脚本在仓库根目录。命令返回零才继续流水线,失败时停止后续交付。实际工程应把断言放进测试目标,让报告显示具体失败字段;不要把联系人原文直接打印出来。
把特殊字符变成独立样本
上面的逗号和分号是故意放进去的。它们在 vCard 结构中有含义,直接拼接文本容易把一个姓名拆成多个组件。使用系统序列化器生成基准,不要先手写替换规则再证明自己的规则正确。
再增加三组输入:带反斜杠的姓名、含换行的公司名称、足够长的多字节字符串。每组只改变一个维度,回读对应属性,并检查联系人总数仍为一,防止换行被错误处理成额外属性或记录。
长行不等于长字符数
vCard 的物理行可能折行,中文字符的字节长度也不同。不要用“每固定数量的 Swift 字符插入换行”实现折行,更不要用逐行 split 自制解析器。若你维护独立编码器,要按声明支持的 vCard 版本检查字节级规则,并把它的输出交给 Contacts 回读。
示例按标签构造字典,仅适用于标签唯一的测试数据。生产联系人可能有重复标签;扩展用例时改成比较“标签、号码”二元组的多重集合,同时保留重复项数量。
区分语义一致与文件一致
同一个联系人重新序列化后,属性顺序和折行方式不一定适合直接做文件快照。优先比较解析后的字段,把文件哈希留给传输完整性检查,而不是拿它判断联系人语义。
批量导出时增加空数组、单联系人和多联系人样本。用测试专用的唯一姓名识别每条记录,不依赖解析结果的数组顺序。分别验证记录数量、字段值和多值项数量,避免字典覆盖掩盖丢失。
系统框架的自回读也不是完整的跨应用互操作证明。若交付对象包含其他导入器,应保存同一批合成文件逐一验收;不支持的标签转换要形成明确契约,不能悄悄忽略。
接入流水线并补齐 iOS 验收
把快速格式检查放在分享功能相关变更之后运行,记录系统及工具链版本。失败时只归档合成 .vcf 和字段错误标识;成功后按流水线保留策略清理目录,不把真实通讯录当测试夹具。
iOS 端还要检查文件扩展名、分享项提供的数据类型,以及目标应用实际读到的文件内容。导入到专用测试环境后核对两个号码、自定义标签和特殊字符,再清除测试记录。模拟器结果不能替代目标设备与目标导入应用的确认。
最后保留三条独立结论:生成的数据能解析,核心字段能回读,分享后的文件能被目标应用正确导入。哪条失败就回到哪层排查,不要通过删除特殊字符或只保留首个号码,让测试表面通过。
常见问题
运行 vCard 序列化测试需要通讯录权限吗?
不需要。示例只创建内存中的联系人并序列化数据,不查询或保存系统通讯录。访问真实通讯录是另一条需要权限的流程。
为什么不直接比较两次导出的 vCard 文件?
属性顺序、换行和折行方式可能变化。应先解析文件,再按字段契约比较姓名、号码和标签;只有明确规定字节格式时才增加字节级断言。