裸辞半年后,我用自动化测试把产品经理“治”好了
去年十月,我在前司熬过了又一个通宵上线的夜晚。那天是周五,窗外黄浦江的夜景美得像假的一样,而我盯着 Jenkins 上那个红得发亮的构建失败标志,心里只有一个念头:这班,不上也罢。
于是,真的裸辞了。
Gap 半年期间,我一边在上海老破小里刷 LeetCode(顺便研究 Rust,真香),一边反思自己这些年在大厂当“人肉回归机”的日子。说白了,我们团队搞移动 App 开发,每次发版前都要手动点几百个 Case,产品经理还总在最后一刻塞需求:“就改一行文案,不用测吧?”——结果上线后用户炸锅,Bug 热搜上本地榜。
现在重新找工作,投了几家 Web3 和 SaaS 公司,面试官一听说我搞过移动端自动化测试,眼睛都亮了。所以今天这篇,不灌鸡汤,不画大饼,就聊聊我怎么用自动化测试把“产品一句话,开发跑断腿”的局面扭转过来的实战经验。顺便夹带点区块链和代码人生的私货,毕竟现在谁不蹭点 Web3 呢(笑)。
为什么移动 App 自动化测试这么难搞?
先说个扎心事实:很多团队嘴上喊着“质量第一”,实际连基础的 UI 自动化都没跑起来。原因无非几个:
- 平台碎片化严重:iOS 和 Android 的控件体系完全不同,iOS 用 XCUIElement,Android 用 UiSelector,写两套脚本?算了吧,谁有那精力。
- 元素定位脆弱:今天用
id="login_btn",明天产品改个文案,ID 换成btn_login_v2,脚本全崩。 - 环境依赖复杂:真机、模拟器、不同 OS 版本、网络状态、权限弹窗……随便一个变量就能让测试挂掉。
- ROI(投入产出比)低:写一个自动化的 Case 可能要 1 小时,手动点只要 2 分钟。老板一问“这玩意儿能提升多少效率?”,你就哑火了。
我在前司就吃过亏。双 11 前一周,产品临时加了个“区块链积分兑换”功能(对,就是那种为了蹭热点硬塞的功能),要求“轻量级不影响主流程”。结果呢?前端改了三个页面,后端加了个智能合约调用接口,测试只给了半天时间。我手动点了三遍,漏了一个边界情况:当用户钱包余额为零时,点击兑换按钮没做防重处理,导致重复请求打爆了后端。线上报错一堆,运维半夜打电话骂街。
那一刻我发誓:下次再干这种事,要么自动化,要么辞职。
我的选择:Appium + Page Object + 自研元素定位策略
经过一番调研(其实就是翻 GitHub 和 Stack Overflow),我最终选了 Appium 作为底层驱动。理由很朴素:跨平台、支持真机、社区活跃,而且能用熟悉的语言写脚本——我用 Python(因为 Vim 写 Python 快啊!)。
但光用 Appium 不够。真正的痛点在于 如何让脚本稳定、可维护、易读。这时候 Page Object 模式就派上用场了。
Page Object 是啥?简单说就是“把页面抽象成对象”
比如登录页,我不再写:
driver.find_element_by_id("username").send_keys("test")
driver.find_element_by_id("password").send_keys("123456")
driver.find_element_by_id("login_btn").click()
而是封装成:
class LoginPage:
def __init__(self, driver):
self.driver = driver
self.username_field = ("id", "username")
self.password_field = ("id", "password")
self.login_button = ("id", "login_btn")
def login(self, username, password):
self._wait_and_send_keys(self.username_field, username)
self._wait_and_send_keys(self.password_field, password)
self._wait_and_click(self.login_button)
def _wait_and_click(self, locator):
element = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable(locator)
)
element.click()
这样做的好处?改动隔离。哪天产品把 ID 改了,我只需要改 LoginPage 里的定义,所有用到登录的地方都不用动。
但 ID 经常变怎么办?自研“多层定位兜底”策略
这才是我踩坑最多的地方。最后我搞了个 fallback 定位机制:
- 优先用
accessibility-id(iOS)或content-desc(Android)——这是给无障碍设计的,通常比较稳定; - 没有的话,用
resource-id(Android)或name(iOS); - 还不行?用 XPath,但必须基于文本或层级结构,避免绝对路径;
- 极端情况:用图像识别(Appium 支持 OpenCV),但慎用,性能差。
举个真实例子:我们有个“区块链资产展示页”,上面有个“刷新”按钮。产品某次改版后,ID 从 refresh_btn 变成了 asset_refresh_v3,但 content-desc 始终是 "Refresh assets"。我就这么写:
refresh_button = (
"accessibility id", "Refresh assets"
) # iOS 和 Android 都认这个
稳如老狗。
实战:搭建一套能扛住产品“骚操作”的测试流水线
光有脚本不够,得跑起来。我的目标是:每次 PR 提交,自动跑核心路径测试;每天凌晨跑全量回归。
CI/CD 集成:Jenkins + Docker + 真机池
前司用的是 Jenkins,虽然老,但胜在稳定。我把测试环境容器化:
- 一个 Docker 镜像包含 Python、Appium、ADB、iOS Simulators;
- 真机通过 USB Hub 接入服务器,用
adb devices和xcrun simctl管理; - Jenkinsfile 里定义 pipeline:
pipeline {
agent { label 'mobile-test' }
stages {
stage('Run Smoke Tests') {
steps {
sh 'pytest tests/smoke/ --device android_11 --report=smoke.html'
}
}
stage('Deploy to Beta if Passed') {
when { expression { currentBuild.result == null || currentBuild.result == 'SUCCESS' } }
steps {
sh './deploy_to_firebase.sh'
}
}
}
}
关键点:只跑冒烟测试(Smoke Test)在 PR 阶段,全量回归放 nightly job。不然开发者等半小时才能 merge,会骂死你。
测试数据管理:别再用 hardcode!
早期我直接在脚本里写 username="testuser123",结果测试账号被风控封了,全挂。后来改用 动态生成测试用户 + Mock 后端:
- 用 Faker 库生成随机用户名、邮箱;
- 对接公司内部的 Mock Server(类似 WireMock),模拟区块链交易响应;
- 敏感操作(如支付)一律 stub 掉,返回预设成功/失败状态。
比如模拟“积分兑换失败”场景:
def test_exchange_fails_when_balance_zero(mock_server):
mock_server.stub("/api/exchange", response={"code": 400, "msg": "Insufficient balance"})
asset_page = AssetPage(driver)
asset_page.click_exchange()
assert asset_page.get_error_message() == "余额不足"
这样,不管后端智能合约逻辑怎么变,我的测试只关心“前端是否正确处理错误”。
区块链?别慌,它只是个 API 调用而已
说到区块链,很多人觉得高大上,其实对我们 App 来说,它就是个慢、贵、不可逆的后端服务。测试重点不是链本身,而是:
- 前端是否正确构造交易参数;
- 是否处理好“交易 pending”状态;
- 用户取消 MetaMask 弹窗后是否回退。
我们的做法是:
- 在测试环境中,用本地 Ganache 私链替代主网;
- 用
ethers.js或web3.py直接发送交易,绕过钱包; - 监听事件日志,验证链上状态变更。
举个例子,测试“NFT 转移”功能:
def test_nft_transfer_success():
# 前提:用户 A 拥有 NFT #42
nft_page = NFTDetailPage(driver)
nft_page.click_transfer()
nft_page.input_recipient("0xRecipientAddress")
nft_page.confirm_transfer()
# 验证:前端显示“Transfer submitted”
assert "submitted" in nft_page.get_status_text()
# 验证:链上所有权变更(通过 web3.py)
owner = web3.eth.contract(address=NFT_ADDR, abi=NFT_ABI).functions.ownerOf(42).call()
assert owner == "0xRecipientAddress"
你看,区块链并没增加多少复杂度,关键还是分层测试:UI 层验证交互,集成层验证链上结果。
效果如何?数据说话
这套体系跑起来三个月后,我们做了个复盘:
| 指标 | 自动化前 | 自动化后 | 提升 |
|---|---|---|---|
| 回归测试时间 | 4人日/次 | 0.5人日/次 | 87.5% ↓ |
| P0/P1 线上 Bug | 平均 3.2 个/月 | 0.8 个/月 | 75% ↓ |
| 发版频率 | 2周/次 | 1周/次 | 2x ↑ |
最爽的是,产品经理再也不敢随便说“就改个小地方,不用测”。因为我们的流水线会自动跑测试,如果挂了,他的 PR 就合不进去。有一次他改了个按钮颜色,结果触发了某个隐藏的动画 Bug,测试挂了,他只好乖乖来找我:“大佬,能帮忙看看吗?”
那一刻,我觉得 Gap 半年值了。
给想入坑的同学几点建议
- 别追求 100% 自动化:先把核心路径(登录、下单、支付)覆盖住,ROI 最高;
- 元素定位策略比工具更重要:花时间设计稳定的 Locator,比换框架有用;
- 和开发约定测试钩子:比如在关键元素上加
test-id="xxx",比靠 XPath 稳一万倍; - 失败用例要可重放:记录截图、日志、设备状态,不然 debug 到崩溃;
- 别怕用图像识别:对于验证码、图表等无法用文本定位的,OpenCV 是救命稻草。
最后说句掏心窝子的话:自动化测试不是银弹,但它能把你从重复劳动中解放出来,去干更有意思的事——比如研究 Rust,或者写这篇博客。
毕竟,代码人生不该只有加班和背锅。希望你在折腾自动化的过程中,也能找到属于自己的节奏。
(完)
注:文中所有代码和配置均已脱敏,但思路完全来自真实项目。如果你也在上海找 Rust/Web3 相关的工作,欢迎交流~

评论 0