XCTest:从简历镀金到真香自动化测试

CDN迷路人
2026-01-13 00:06
阅读 454

上周五晚上十一点半,我瘫在工位上盯着 Xcode 里红得发紫的测试报告,脑子里只有一个念头:“这破玩意儿怎么又崩了?”
——别误会,不是我的代码崩了,是我的 UI 自动化测试脚本 又在模拟器里跑飞了。而明天就是 Sprint Review,产品经理已经把 Demo 视频剪好了(虽然根本没跑通),就等我这边“自动验证”环节撑场面。

我是上海某 211 软件工程研二学生,目前在一家做跨境支付的创业公司实习,日常主力开发机是 MacBook Pro M2(Windows?那只是我用来测 IE 兼容性的古董)。实验室项目用 Spring Boot 搞后端微服务,前端是 Vue + Electron,移动端 iOS 端则是我主攻方向。最近团队被老板“赋能”了一个新 KPI:提升自动化测试覆盖率至 70% 以上,否则“简历优化”警告。

于是,我被迫深入研究 XCTest —— Apple 官方钦定的测试框架。本以为只是写几个 XCTAssert 就完事,结果发现水比黄浦江还深。


为什么是 XCTest?而不是 Appium 或 Detox?

说实话,一开始我本能地想上 Appium。毕竟简历上写“熟悉跨平台自动化测试框架”听起来更高级,而且团队后端同事用 Spring Boot 写接口时,顺手用 TestContainers 做集成测试,生态很统一。但现实狠狠打了我的脸:

  • 速度慢到怀疑人生:Appium 启动一次 iOS 测试要 30s+,而 XCTest 直接跑在模拟器/真机上,秒级启动。
  • 稳定性堪忧:元素定位频繁失效,XPath 在 iOS 上根本靠不住,Accessibility ID 又得开发配合加。
  • Apple 生态锁死:XCTest 能直接调用 Swift 代码、访问私有 API(调试时)、甚至和 SwiftUI 预览联动。

更重要的是——App Store 审核越来越看重测试完备性。去年有个版本因为手动测试漏掉一个边界 case,上线后 Crash 率飙升,被 Apple 退回两次。从此老板立下规矩:没有自动化测试覆盖的核心路径,不准合入主干

行吧,既然逃不掉,那就干。


XCTest 的三层结构:别只写单元测试!

很多同学(包括半年前的我)以为 XCTest = 单元测试。错!XCTest 实际包含三个层级:

类型 用途 运行环境 典型场景
Unit Test 验证函数/类逻辑 模拟器或 Mac 工具类、算法、ViewModel
UI Test 模拟用户操作流程 模拟器或真机 登录、支付、导航流
Snapshot Test(需第三方库) 视觉回归测试 模拟器 UI 布局、主题切换

我们团队主要攻坚的是 UI Test,因为这才是产品经理最关心的“用户能不能点进去”。

但 UI Test 有个致命痛点:异步等待。比如点击“支付”按钮后,网络请求需要 2 秒,你怎么知道页面跳转完成了?早期我用 sleep(3),结果 CI 上偶尔超时,本地又浪费时间。后来才学会用 XCTWaiter + expectation

func testPaymentSuccess() {
    let app = XCUIApplication()
    app.launch()

    // 点击支付按钮
    app.buttons["payButton"].tap()

    // 等待成功提示出现(最多 5 秒)
    let successLabel = app.staticTexts["支付成功"]
    let expectation = XCTNSPredicateExpectation(
        predicate: NSPredicate(format: "exists == true"),
        object: successLabel
    )
    
    XCTWaiter().wait(for: [expectation], timeout: 5.0)
    
    XCTAssertTrue(successLabel.exists)
}

这段代码现在成了我们团队的模板。别再用 sleep 了!面试官看到会皱眉的(别问我怎么知道的)。


性能优化:让 CI 不再是“等待的艺术”

说到 CI,我们用的是 GitHub Actions。但每次 push 后,iOS 测试 job 跑 8 分钟,其中 6 分钟在等模拟器启动。痛定思痛,我做了三件事:

  1. 复用模拟器设备:通过 xcrun simctl 预创建一个干净的模拟器,CI 中直接复用,省去每次安装的开销。
  2. 并行化测试:把 UI Test 按业务模块拆成多个 target,GitHub Actions 矩阵并行跑。
  3. 跳过非必要测试:通过 #if DEBUG 包裹某些耗时长的视觉测试,只在 nightly build 中运行。

效果立竿见影:平均测试时间从 8 分钟降到 2 分 30 秒。老板看了直呼“技术驱动降本增效”,当场给我加了 200 块团建费(感动哭)。

对了,Spring Boot 后端同事看我折腾,还开玩笑说:“你们 iOS 测试终于赶上我们 Java 的 JUnit 了?” 我回他:“等着,下次我用 Swift 写个 Mock Server,直接集成到 XCTestCase 里,不用你们 TestContainers 了!”(其实已经在做了)


真机 vs 模拟器:求职时别踩这个坑

很多同学在简历上写“精通 iOS 自动化测试”,结果面试一问“真机测试怎么做”,就卡壳了。

XCTest 默认只能在模拟器跑 UI Test。真机测试需要额外配置

  • 开发者账号加入设备 UDID
  • Target 的 Bundle Identifier 必须匹配 Provisioning Profile
  • 测试 Target 也要签名(很多人忘了这点!)

我们团队因为要测 Face ID 支付流程,必须上真机。于是我在 Jenkins 上配了一台 iPhone 14 Pro 当“奴隶机”(运维小哥吐槽说像矿机)。每次跑测试前,脚本自动 unlock 设备、清除数据、安装最新 build。

但!App Store 审核严禁自动化脚本操控真机提交审核包。所以我们 CI 里只跑模拟器测试,真机测试仅用于内网 Beta 版验证。

这点一定要分清,不然简历写“支持真机自动化测试”可能被质疑合规风险——我秋招面某大厂时就被 challenge 过这个问题。


和 SwiftUI 的甜蜜(且痛苦)协作

我们新功能用 SwiftUI 重写了支付页。本以为声明式 UI 会让测试更简单,结果发现 SwiftUI 的 accessibilityIdentifier 默认是空的

这意味着 app.buttons["payButton"] 根本找不到元素。解决方案是在 View 里显式加 .accessibilityIdentifier("payButton")

Button("确认支付") {
    viewModel.pay()
}
.accessibilityIdentifier("payButton")

但开发同学嫌麻烦,说“影响代码整洁”。于是我写了篇内部 Wiki,标题就叫《为了你的简历能写“高覆盖率测试”,请加上这行代码》,果然奏效(笑)。

另外,SwiftUI 的动画也会干扰测试。比如淡入效果导致元素短暂不可交互。解决办法是:在测试环境下关闭动画

// 在 AppDelegate 或 App struct 中
#if DEBUG
if ProcessInfo.processInfo.environment["RUNNING_UI_TESTS"] == "1" {
    UIView.setAnimationsEnabled(false)
    // SwiftUI 也需要特殊处理
}
#endif

然后在 UI Test 启动时注入环境变量:

override func setUp() {
    continueAfterFailure = false
    let app = XCUIApplication()
    app.launchEnvironment = ["RUNNING_UI_TESTS": "1"]
    app.launch()
}

最后:自动化测试不是银弹,但能让你睡好觉

折腾两个月后,我们的核心路径自动化覆盖率从 30% 提升到 78%。上周双 11 大促,凌晨三点收到告警说支付回调异常,我第一反应不是查日志,而是跑了一遍 UI Test——果然复现了问题,定位到是某个 header 没传。

那一刻,我觉得之前写的每一行 XCTestCase 都值了。

如果你也在准备求职,简历上写“使用 XCTest 构建 CI/CD 自动化测试体系,提升回归效率 300%” 绝对比 “熟悉 iOS 开发” 有杀伤力。尤其是投递对质量要求高的外企或金融公司。

当然,别指望靠它拿 offer。但至少,当面试官问“你怎么保证代码质量”时,你能笑着掏出一段跑通的 UI Test 视频——而不是只会说“我认真写单元测试”。

至于我?下周还要重构测试用例的数据驱动部分,顺便研究怎么用 Swift Package 把通用测试工具抽出来。毕竟,研二了,得为秋招攒点硬核项目

(写完这篇,终于可以关电脑了。窗外凌晨一点的上海,连外卖小哥都下班了。)

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝