iOS自动化测试:XCTest框架详解 —— 一个老广奶爸的深夜代码手记

生产环境勿扰
2025-12-15 17:14
阅读 514

坐标广州西关,凌晨1点17分。两个娃终于睡了,老婆在隔壁房间打起了轻微的呼噜。我轻手轻脚地摸出MacBook,打开Xcode,屏幕的光映在墙上,像小时候骑楼下的那盏煤油灯。


去年十月的一个周五晚上,我正蹲在儿童房门口,一边拍着小宝的背,一边用手机回公司钉钉消息。那天是发版前最后一天,QA同事突然甩来一条消息:“老大,登录模块又崩了,这次是iOS端,用户输错密码三次后直接闪退。”

我心里“咯噔”一下。这功能我亲手写的,逻辑不复杂,但偏偏没加异常处理。更糟的是——我们根本没有写UI自动化测试

那会儿项目已经进入第三期,团队6个人,迭代节奏越来越快。老板天天在群里喊“要提效、要质量”,可现实是:手动回归测试占了QA一半时间,开发自测全靠“我觉得没问题”。

回到家,老婆看我脸色不对,一边给大宝擦脸一边问:“又出事了?”
我苦笑:“嗯,上线前翻车。”
她叹了口气:“你不是说学自动化测试吗?怎么还没搞?”

我哑口无言。嘴上说着“要学”,结果每天下班回来就是陪娃、做饭、洗尿布。等娃睡了,眼皮也快粘上了。成年人的flag,往往死在哄睡之后


转机:3500块的房租和22k的offer

事情的转折发生在去年年底。有猎头联系我,说有家公司急招懂iOS自动化测试的中级工程师,月薪从15k涨到22k,前提是能独立搭建XCTest框架并落地到项目中

我和老婆商量了一晚。当时我们租在荔湾老城区一栋90年代的楼梯房,月租3500,厨房小得转不开身。两个娃的奶粉、早教班、老人医药费……每一分都算得清楚。

“试试吧,”她说,“大不了失败,但至少试过。”

于是,我给自己定了个死线:三个月内,在现有项目里跑通XCTest自动化测试流水线


从零开始:XCTest不是魔法,是耐心

很多人以为XCTest就是写几个XCTAssertEqual就完事了。但真实项目哪有这么简单?

我们的App是个本地生活服务平台,核心路径包括:启动 → 登录/注册 → 搜索商家 → 下单支付。每一步都可能出问题,尤其登录——网络波动、验证码失效、第三方授权回调超时……

第一个坑:异步操作怎么测?

比如登录接口是异步的。一开始我这样写:

func testLoginSuccess() {
    loginViewModel.login(username: "test", password: "123456")
    XCTAssertTrue(loginViewModel.isLoggedIn)
}

结果永远失败。因为login()刚发起请求,断言就执行了,此时isLoggedIn还是false。

后来才明白:XCTest支持异步等待。要用XCTestExpectation

func testLoginSuccess() {
    let expectation = XCTestExpectation(description: "Login completes")
    
    loginViewModel.login(username: "test", password: "123456") { success in
        XCTAssertTrue(success)
        expectation.fulfill()
    }
    
    wait(for: [expectation], timeout: 10)
}

那一刻,我在凌晨两点的书桌前差点跳起来——终于等到它绿了

第二个坑:UI测试的“脆弱性”

UI测试更头疼。比如点击“立即下单”按钮,有时弹窗慢半拍,测试就崩了。

我一度想放弃。直到看到Apple官方文档里那句:“Use XCUIElementQuery with retries, not hardcoded sleeps.

于是我封装了一个等待元素出现的方法:

extension XCUIApplication {
    func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 10) -> Bool {
        let existsPredicate = NSPredicate(format: "exists == true")
        let expectation = XCTNSPredicateExpectation(predicate: existsPredicate, object: element)
        let result = XCTWaiter.wait(for: [expectation], timeout: timeout)
        return result == .completed
    }
}

然后在测试里这样用:

let app = XCUIApplication()
app.launch()

let orderButton = app.buttons["立即下单"]
XCTAssertTrue(app.waitForElement(orderButton))
orderButton.tap()

从此,测试不再因为“慢0.5秒”而随机失败


综合落地:让测试真正服务于项目

光会写测试用例不够,关键是如何嵌入现有项目流程

我们做了三件事:

  1. 分层测试策略

    • 单元测试(Unit Test):覆盖ViewModel、工具类,目标覆盖率≥70%
    • UI测试(UI Test):只覆盖核心路径(登录→下单→支付),保证主干稳定
    • 手动测试:留给边缘场景和探索性测试
  2. CI集成
    在Jenkins里加了一步:每次PR合并前,自动跑XCTest。如果失败,直接阻断合并。
    老板一开始嫌慢,但一次阻止了因日期格式错误导致的支付崩溃后,他主动说:“多花两分钟,值。”

  3. 团队共建
    我拉着QA一起写测试用例。他们熟悉业务场景,我负责技术实现。比如“优惠券叠加使用”这种复杂逻辑,QA列了8种组合,我写成参数化测试:

func testCouponCombination() {
    let cases = [
        ("满100减20", "折扣券8折", expected: 72.0),
        ("满200减50", "无门槛券10元", expected: 140.0)
    ]
    
    for (couponA, couponB, expected) in cases {
        let total = calculateTotal(with: [couponA, couponB])
        XCTAssertEqual(total, expected, "Failed for \(couponA) + \(couponB)")
    }
}

当测试变成团队资产,而不是开发的负担,它才真正活了


那些深夜的顿悟

上周五晚上,小宝发烧到39度。我一边用温水给他擦身子,一边心里盘算:今天本该完成支付模块的UI测试,但肯定没时间了。

凌晨三点,他终于睡安稳。我坐在客厅小凳子上,打开电脑,发现白天写的测试居然通过了——原来昨天漏掉的一个边界条件,被单元测试自动捕获了。

那一刻我没觉得累,反而有点想哭。

不是因为代码多牛,而是我终于把“救火队员”的角色,慢慢变成了“防火墙”的建造者


给同行的一点真心话

如果你和我一样:

  • 是个普通程序员,不是大厂高P
  • 家里有娃要哄,没整块时间学习
  • 项目赶进度,老板说“先上线再说”

我想说:别等完美时机

从一个最痛的点开始。比如你们总在登录页出bug?那就先写登录的单元测试。哪怕只有3个用例,也比没有强。

XCTest不是银弹,但它能帮你把重复踩的坑,变成自动巡逻的哨兵

而且你知道吗?当你能在晨会上说“这个改动我有测试覆盖,放心上线”,那种底气,比涨薪还爽。


现在,我的XCTest框架已经跑在公司三个核心项目里。上个月,团队线上崩溃率下降了62%。老板请我们吃了顿炳胜,席间笑着说:“没想到你这个老广码农,还能搞自动化。”

回家路上,我牵着大宝的手,走在恩宁路的青石板上。风吹过来,带着凉茶铺的甘草香。

我知道,明天还有无数个需求、无数个bug等着我。但至少今晚,我可以安心地等两个娃睡着,再打开Xcode——不是为了救火,而是为了筑墙

毕竟,一个奶爸程序员最大的浪漫,就是在生活的鸡飞狗跳里,悄悄为世界多加一道保障。

后记:本文所有代码均来自真实项目脱敏。如果你也在广州做iOS开发,欢迎约个早茶,聊聊XCTest,或者单纯吐槽带娃有多难。记得,我们不是一个人在战斗。

评论 0

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