当一个纯前端被迫搞移动测试自动化:从懵逼到能跑通的血泪史

接口调不通
2025-12-16 16:29
阅读 989

大家好,我是小杭,一个坐标杭州、常年在网易和阿里生态圈里晃悠的纯前端码农。平时写 Vue 和 React 写到手抽筋,耳机里不是 Lo-fi 就是《The Weeknd》,梦想是写出人见人夸、PR 一过就 merge 的优雅代码。但现实嘛……你懂的,老板说“全栈才是未来”,于是我去年开始啃 Node.js,一边搭后端接口,一边被各种 DevOps 工具链按在地上摩擦。

上周五晚上十点半,我正对着屏幕调一个诡异的白屏 bug(又是安卓 12 上的 WebView 兼容问题),产品经理突然在钉钉群里 @ 我:“小杭啊,咱们 App 下个月要上新功能,测试那边人力不够,能不能搞个自动化测试?最好覆盖核心路径,别又像上次双11那样,用户反馈支付页卡死,我们凌晨三点爬起来 hotfix……”

我当时内心 OS:我是前端啊!连 adb 命令都得查文档的人,让我搞移动自动化?

但转念一想——这不正好练手 Node + 自动化测试的好机会吗?而且听说隔壁组用自动化测试把回归时间从两天压缩到两小时,上线前再也不用跪求测试小姐姐加班了。行吧,硬着头皮上!


起因:为什么非得搞自动化?

先说背景。我们团队维护一个混合 App(H5 + 原生壳),主要用户在 iOS 和安卓两大平台。以前测试全靠人工点点点,每次发版前 QA 同学都要重复走十几条主路径:登录 → 首页 → 商品详情 → 加购 → 支付 → 订单确认。一次完整回归至少半天,还容易漏测边缘场景。

更惨的是,有一次因为某个安卓机型的键盘弹出导致页面错位,QA 没覆盖到那台真机,结果上线后差评如潮。运维大哥半夜打电话骂我:“你这前端写的什么鬼布局?!”(其实锅在原生层没适配软键盘事件……但谁听你解释)

所以,自动化测试成了刚需。但问题来了:作为一个前端,我该用啥工具?


工具选型:Appium vs. 其他?Spring Boot 竟然也掺和进来了?

一开始我以为移动自动化就是写脚本控制手机点按钮,搜了一圈发现主流方案是 Appium —— 一个基于 WebDriver 协议的跨平台自动化测试框架。它支持 iOS 和 Android,还能用 JavaScript 写测试脚本(对我这种 JS 狗太友好了!)。

但很快打脸了。

我们后端是 Spring Boot 架构,测试数据(比如用户 token、订单 ID)需要从测试环境的数据库或接口获取。如果只用 Appium,就得手动准备测试账号、模拟支付回调,效率极低。更麻烦的是,有些测试场景需要预置特定状态,比如“用户有未支付订单”或“优惠券已过期”。

这时候,我灵光一闪:能不能让 Spring Boot 提供一个测试辅助服务? 比如暴露几个 REST 接口,一键创建测试用户、清空购物车、模拟支付成功等。这样 Appium 脚本在运行前先调这个服务,准备好上下文,再启动 App 操作。

团队里的 Java 老哥一听,眼睛一亮:“这思路可以啊!我们正好有个 test-support 模块,加几个 endpoint 不费事。” 于是,Spring Boot 成了自动化测试的“后勤部长”,负责造数据;Appium 则是“前线士兵”,负责操作 UI。

📌 关键点:自动化测试不是单打独斗,而是前后端+测试工具的协同作战。尤其在混合开发中,光会控件点击远远不够。


实战:从零搭建自动化流水线

第一步:环境准备(踩坑预警)

Appium 安装看似简单,实则暗坑无数。我在 macOS 上装完,启动时直接报:

Error: Could not find a connected Android device.

原来忘了开 USB 调试,还得在开发者选项里点“USB 调试(安全设置)”。iOS 更离谱,Xcode 版本、Simulator 镜像、WebDriverAgent 编译……折腾了整整一天才跑通第一个 demo。

建议:用 Docker 跑 Appium Server,省去本地环境冲突。命令如下:

docker run -d --name appium-server \
  --privileged \
  -p 4723:4723 \
  -v /dev/bus/usb:/dev/bus/usb \
  appium/appium

第二步:写第一个测试脚本(JS 版)

得益于 WebdriverIO(Appium 的 JS 客户端),语法和 Cypress/Puppeteer 很像,上手飞快:

// test/login.e2e.js
const { remote } = require('webdriverio');

describe('用户登录流程', () => {
  let driver;

  beforeAll(async () => {
    driver = await remote({
      capabilities: {
        platformName: 'Android',
        'appium:deviceName': 'Pixel_4_API_30',
        'appium:appPackage': 'com.mycompany.app',
        'appium:appActivity': '.MainActivity',
        'appium:automationName': 'UiAutomator2'
      }
    });
  });

  it('应能成功登录', async () => {
    // 调用 Spring Boot 测试服务,获取测试账号
    const testUser = await fetch('http://test-support:8080/api/v1/test-user')
      .then(res => res.json());

    // 在 App 中输入账号密码
    const phoneInput = await driver.$('~phone_input');
    await phoneInput.setValue(testUser.phone);

    const pwdInput = await driver.$('~password_input');
    await pwdInput.setValue(testUser.password);

    const loginBtn = await driver.$('~login_button');
    await loginBtn.click();

    // 断言首页出现用户名
    const welcomeText = await driver.$('~welcome_text');
    expect(await welcomeText.getText()).toContain(testUser.name);
  });

  afterAll(async () => {
    await driver.deleteSession();
  });
});

注意这里用了 ~ 开头的 accessibility id,这是推荐做法,比 xpath 或 resource-id 更稳定(尤其在多语言/动态 ID 场景下)。


第三步:Spring Boot 测试服务怎么写?

后端老哥甩给我一个简单的 Controller:

// TestSupportController.java
@RestController
@RequestMapping("/api/v1")
public class TestSupportController {

    @Autowired
    private UserService userService;

    @PostMapping("/test-user")
    public ResponseEntity<TestUser> createTestUser() {
        // 创建一个带预设权限的测试用户
        TestUser user = userService.createTestUser("auto_test_" + System.currentTimeMillis());
        return ResponseEntity.ok(user);
    }

    @DeleteMapping("/cart/{userId}")
    public ResponseEntity<Void> clearCart(@PathVariable String userId) {
        cartService.clear(userId);
        return ResponseEntity.noContent().build();
    }

    // 其他辅助接口...
}

这个服务只在测试环境开启,生产环境完全隔离。通过 Docker Compose 把 Appium、Spring Boot Test Support、MySQL 测试库串起来:

# docker-compose.test.yml
version: '3'
services:
  appium:
    image: appium/appium
    ports:
      - "4723:4723"
    privileged: true
    volumes:
      - /dev/bus/usb:/dev/bus/usb

  test-support:
    build: ./test-support-service
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=test

  mysql-test:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: test_db

跑测试时,一条命令搞定:

docker-compose -f docker-compose.test.yml up -d
npm run test:e2e

平台差异:安卓和 iOS 的“双标”现场

自动化最大的痛,就是平台行为不一致

  • 安卓:WebView 上下文切换很稳,但不同厂商 ROM 对权限弹窗处理不同(比如华为会自动拒绝位置权限,而小米要手动点)。
  • iOS:Simulator 上跑得飞快,但真机调试必须用 Xcode 签名,而且每次 App 更新都要重新信任证书。

有一次我写了个“点击地址栏跳转 H5”的用例,在 iOS Simulator 上完美通过,结果在安卓真机上死活找不到元素。最后发现:安卓的 WebView 默认不暴露 accessibility id!必须在原生层加一行:

// Android WebView 设置
webView.setWebContentsDebuggingEnabled(true);
webView.getSettings().setDomStorageEnabled(true);

还有一次,iOS 15 更新后,系统弹窗的层级变了,原来的 driver.dismissAlert() 失效,得改用坐标点击。真是血压拉满……

💡 经验:一定要在 CI 流程中接入多机型云测平台(比如阿里云 Mobile Testing、Firebase Test Lab),别只依赖本地模拟器。


效果:从“提心吊胆”到“安心睡觉”

搞了两周后,我们的核心路径自动化覆盖率到了 70%。现在每次 PR 合并前,CI 自动跑一遍 E2E 测试:

  • 如果失败,直接 block 合并,并附上录屏和日志
  • 如果成功,测试同学只需 focus 在探索性测试和边缘 case

上周发版,全程零 P0 事故。测试组长请我喝了杯瑞幸,说:“小杭,你这自动化比我们手动测得还细!”

最爽的是,我不用再半夜接运维电话了。双11 那天晚上,我安心在家听歌 coding,朋友圈看到运维在晒加班泡面——而我的自动化脚本正在云端默默跑着第 50 轮回归。


心得:前端搞自动化,优势在哪?

很多人觉得自动化是测试工程师的事,但作为前端,我反而有独特优势:

  1. 懂 UI 结构:知道哪些元素该加 test-id,哪些交互有陷阱(比如防抖按钮、懒加载列表)
  2. 熟悉异步逻辑:H5 页面大量 AJAX 和动画,能写出更健壮的等待策略(比如 waitForExist + waitForDisplayed 组合)
  3. 工程化思维:用 Jest + WebdriverIO + Allure Report 搞出漂亮的测试报告,连产品都能看懂

当然,也踩了不少坑:

  • 别用 xpath 定位!动态 class 名分分钟让你崩盘
  • 真机测试必须考虑网络延迟,加合理的 wait timeout
  • 日志!日志!日志!重要的事情说三遍,没日志的自动化等于盲人摸象

最后:全栈不是梦,但别一个人硬扛

回看这段经历,从“我是前端为啥要搞这个”到“真香”,最大的感悟是:现代开发早已不是单兵作战。前端、后端、测试、运维的边界越来越模糊,谁能快速整合工具链、解决实际问题,谁就能在 deadline 前笑着下班。

如果你也在杭州,纠结要不要学 Node 或自动化,我的建议是:先动手,再纠结。哪怕只是写个脚本自动清理本地缓存,也是迈向全栈的一小步。

对了,Spring Boot 那边的老哥最近在招人,说我们这套测试方案他们想推广到全公司……说不定下次见面,我就不是“纯前端”了,而是“那个搞自动化的小杭”。

(完)


附:常用配置速查表

项目 配置项 说明
Appium Capabilities automationName Android 用 UiAutomator2,iOS 用 XCUITest
等待策略 implicitWait vs explicitWait 建议用 explicit(如 waitForExist),避免隐式等待拖慢整体
测试数据 Spring Boot Test Support 提供 /api/v1/clean-db, /api/v1/mock-payment
CI 集成 GitHub Actions / Jenkins 每次 push 触发,失败自动通知钉钉群
报告生成 Allure Report 可视化测试步骤、截图、日志

代码可读性 tip:给所有测试用例加 describeit 的清晰描述,三个月后的你会感谢现在的自己。

评论 0

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