裸辞半年后,我用自动化测试把产品经理“治”好了

萧浩天
2025-12-21 16:01
阅读 1636

去年十月,我在前司熬过了又一个通宵上线的夜晚。那天是周五,窗外黄浦江的夜景美得像假的一样,而我盯着 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 定位机制

  1. 优先用 accessibility-id(iOS)或 content-desc(Android)——这是给无障碍设计的,通常比较稳定;
  2. 没有的话,用 resource-id(Android)或 name(iOS);
  3. 还不行?用 XPath,但必须基于文本或层级结构,避免绝对路径;
  4. 极端情况:用图像识别(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 devicesxcrun 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 弹窗后是否回退。

我们的做法是:

  1. 在测试环境中,用本地 Ganache 私链替代主网;
  2. ethers.jsweb3.py 直接发送交易,绕过钱包;
  3. 监听事件日志,验证链上状态变更。

举个例子,测试“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 半年值了。


给想入坑的同学几点建议

  1. 别追求 100% 自动化:先把核心路径(登录、下单、支付)覆盖住,ROI 最高;
  2. 元素定位策略比工具更重要:花时间设计稳定的 Locator,比换框架有用;
  3. 和开发约定测试钩子:比如在关键元素上加 test-id="xxx",比靠 XPath 稳一万倍;
  4. 失败用例要可重放:记录截图、日志、设备状态,不然 debug 到崩溃;
  5. 别怕用图像识别:对于验证码、图表等无法用文本定位的,OpenCV 是救命稻草。

最后说句掏心窝子的话:自动化测试不是银弹,但它能把你从重复劳动中解放出来,去干更有意思的事——比如研究 Rust,或者写这篇博客

毕竟,代码人生不该只有加班和背锅。希望你在折腾自动化的过程中,也能找到属于自己的节奏。

(完)

注:文中所有代码和配置均已脱敏,但思路完全来自真实项目。如果你也在上海找 Rust/Web3 相关的工作,欢迎交流~

评论 0

最热最新
暂无评论
萧浩天Lv.1
0
影响力
0
文章
0
粉丝