从零搭建移动应用自动化测试体系,我们踩过的坑和收获的经验

贪心没贪够
2025-06-18 20:13
阅读 533

作为一位全栈开发工程师,在过去的几年里我有幸参与过多个中大型移动项目的开发工作。随着项目体量越来越大、需求变更越来越频繁,手动测试逐渐暴露出效率低下、覆盖率低、回归测试成本高等问题。于是我们开始思考:是否可以通过引入自动化测试来提升整体质量和交付效率?

这篇文章就源于我在一个电商类APP项目中的真实经历——如何从零到一构建一套适用于Android和iOS的自动化测试体系,以及期间遇到的各种挑战和解决方案。


背景介绍:为什么我们需要自动化测试?

背景介绍:为什么我们需要自动化测试?

这个项目是一个面向全国用户的线上电商平台,用户量超过千万,日活几十万。初期由于团队规模小、产品迭代快,测试环节基本以人工为主,偶尔用Jest写几个简单的单元测试。但随着功能越来越多、版本更新频率加快(平均两周一次上线),测试压力急剧上升:

  • 每次上线前QA需要重复跑几十个核心流程,容易出错
  • 多人协作时经常出现改动引发的连锁故障
  • 团队对代码改动带来的风险缺乏信心
  • 新入职同学对业务流程不熟悉,影响测试质量

最终我们决定引入UI自动化测试,并逐步补充单元测试与接口测试。目标是实现“基础业务流程自动化+核心模块单元测试+持续集成”。


遇到的挑战:现实远比预期复杂

遇到的挑战:现实远比预期复杂

理想很丰满,但现实一开始就给我们泼了冷水。刚开始选型的时候,我们调研了几种主流方案:

  1. Appium + WebDriver + Page Object
  2. Detox + Jest(React Native 项目)
  3. XCUITest + XCTest(原生 iOS)
  4. Espresso(原生 Android)

考虑到项目主体使用 React Native 开发,同时需要支持iOS和Android双平台,最终选择 Detox + Jest 的组合。但在具体落地过程中,还是遇到了不少坑:

✦ 元素定位困难

React Native 渲染出来的UI组件在原生层并不是一一对应的,导致部分元素通过常规方式定位不到。比如按钮的文字可能被包裹在一个View里,找不到唯一ID。

✦ 自动化测试执行慢

尤其是在模拟器上运行时,点击、输入等动作都明显延迟,严重影响执行效率。一个原本5分钟能完成的手工测试流程,自动化居然要10多分钟。

✦ 测试环境不稳定

某些设备或操作系统版本下会出现随机失败的情况,例如某个iOS真机上某条测试案例总是在CI环境中失败,却无法本地复现。

✦ 真机兼容性问题严重

不同安卓厂商的定制系统(比如MIUI、EMUI)对权限管理特别严格,导致自动化测试过程中经常弹出对话框阻碍执行。

这些问题一度让我们怀疑是否应该继续坚持自动化这条路,不过经过几次技术评估和讨论后,我们决定再试一试,换一种更灵活的方式去解决问题。


解决方案:定制化+分阶段实施

解决方案:定制化+分阶段实施

我们的策略是先从小范围做起,逐步扩展测试覆盖面。整个自动化测试体系建设分为了三个阶段:

  1. 第一阶段:基础框架搭建

    • 技术栈确定为 Detox + Jest + GitHub Actions
    • 所有测试脚本统一用JavaScript编写
    • 使用PageObject模式组织结构,提高可维护性
  2. 第二阶段:核心路径覆盖

    • 选取登录、下单、支付、我的订单等关键路径进行覆盖
    • 引入Mock服务拦截网络请求,保证测试数据一致性
    • 初期集中在模拟器上运行,降低调试成本
  3. 第三阶段:扩展与优化

    • 接入真机云测平台(如Testin、阿里云)
    • 补充单元测试和API测试,形成完整测试金字塔
    • 接入CI/CD流程,每次PR自动触发相关用例执行

关键代码示例

关键代码示例

这里分享一些实际使用的代码片段,帮助你理解整体结构和思路。

📄 e2e/config.json(Detox配置)

{
  "testRunner": "jest",
  "runnerConfig": "e2e/config.js",
  "configurations": {
    "ios.sim.debug": {
      "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/myapp.app",
      "build": "xcodebuild -project ios/myapp.xcodeproj -scheme myapp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build",
      "type": "ios.simulator",
      "device": {
        "type": "iPhone 13"
      }
    },
    "android.emu.release": {
      "binaryPath": "android/app-release.apk",
      "build": "cd android && ./gradlew assembleRelease assembleAndroidTest -Dorg.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8",
      "type": "android.emulator",
      "device": {
        "avdName": "Pixel_3a_API_30_x86"
      }
    }
  }
}

📄 e2e/login.test.js

describe('Login flow', () => {
  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('should login successfully with valid credentials', async () => {
    await expect(element(by.id('loginButton'))).toBeVisible();
    await element(by.id('usernameInput')).typeText('testuser');
    await element(by.id('passwordInput')).typeText('password123');
    await element(by.id('loginButton')).tap();

    // 断言跳转到了首页
    await expect(element(by.text('首页'))).toBeVisible();
  });
});

📄 页面对象封装(Page Object Pattern)

// e2e/pages/LoginPage.js
class LoginPage {
  constructor() {
    this.usernameInput = element(by.id('usernameInput'));
    this.passwordInput = element(by.id('passwordInput'));
    this.loginButton = element(by.id('loginButton'));
  }

  async loginWith(username, password) {
    await this.usernameInput.typeText(username);
    await this.passwordInput.typeText(password);
    await this.loginButton.tap();
  }
}

module.exports = new LoginPage();

踩过的坑与实战经验

💥 1. “点击无效”之痛

早期我们写了一个“点击商品加入购物车”的测试,结果总是失败。调试发现按钮明明可见,但点击没反应。

原因分析: 原来是React Native里有个中间层View包裹着按钮,真正点击的是子元素。默认查找时只找到了父级,自然点不动。

解决方法: 增加更具体的标识符,在RN组件上加上 testID="addToCartBtn",这样就可以精确定位。

小建议:给每个交互控件设置合理的 testID 或 accessibilityLabel,这对自动化测试非常重要!


💥 2. 网络请求干扰测试流程

有时候页面加载时会发起很多异步请求,比如埋点SDK、广告加载等。这些请求会导致页面迟迟无法进入下一步操作,直接让测试失败。

解决方法: 我们在测试环境下,使用 fetch-mock 对外部请求做了拦截:

import fetchMock from 'fetch-mock';

beforeAll(() => {
  fetchMock.restore(); // 重置之前的mock状态
  fetchMock.get('/api/user/info', { name: 'testuser' });
  fetchMock.post('/api/cart/add', { success: true });
});

afterAll(() => {
  fetchMock.restore();
});

也可以结合 Detox 提供的 mocking 功能,或者接入独立的 Mock Server。


💥 3. iOS 上键盘弹出导致断言失败

iOS 上输入文本后软键盘不会自动收起,有时会遮挡后续元素的点击区域,导致元素不可见。

解决方案: 在测试中主动关闭软键盘:

await device.executeDeviceCommand('simulator: dismissKeyboard'); // 仅限iOS模拟器

如果是真机的话,可以尝试调用原生API隐藏软键盘:

await device.executeScript("UIATarget.localTarget().frontMostApp().keyboard().buttons()['Done'].tap()");

实施后的效果与收益

应用性能监控-1

经过几个月的努力,我们逐步将核心路径用自动化覆盖起来,并在CI中集成了Detox测试任务。以下是几个显著的变化:

测试效率提升5倍以上:以前手工跑一遍核心流程要30分钟,现在只需6分钟左右,且能并行执行
Bug暴露得更早:每次代码合入后都能快速发现回归问题,大幅减少了发布前才发现的故障
团队信心增强:新功能提交前大家都会跑一下相关的自动化用例,不再担心误操作影响老流程
节省人力成本:原本每次上线需要2位QA做回归测试,现在减少为1人简单复查即可

特别是在节假日大促版本上线前,自动化测试帮我们及时发现了“优惠券叠加异常”、“下单页价格计算错误”等多个重要缺陷。


给读者的几点建议

  1. 不要盲目追求覆盖率,优先保障核心业务路径稳定可靠才是王道。
  2. 提前规划标识符,无论是 testID 还是 accessibilityLabel,都应该作为UI开发的一部分。
  3. 测试环境要做隔离,别让生产环境的真实数据干扰你的测试流程。
  4. 善用Mock工具,尤其是处理第三方服务或高并发依赖时。
  5. 不要忽视真机测试的价值,毕竟模拟器跟真实设备之间还有很多差异。
  6. 定期重构测试代码,良好的结构和清晰的命名会让后期维护轻松许多。
  7. 建立报警机制,一旦自动化测试失败,及时通知负责人,别让问题堆积。

后续计划与趋势展望

目前我们的测试体系建设还处在成长期,接下来有几个方向值得推进:

🔍 可视化测试报告:集成Allure或ReportPortal,提升问题追踪能力
📱 引入跨平台UI组件库:比如Galio、React Native Paper,减少适配成本
🤖 探索AI辅助测试:利用图像识别动态生成测试用例或自动修复脚本
🛠️ 接入Figma设计规范比对工具:在自动化测试中加入视觉校验

另外,随着React Native向Turbo Modules、Fabric架构演进,Detox也在不断升级,未来我们也会持续跟进官方进展,保持自动化测试的先进性和稳定性。


结语:自动化测试不是一蹴而就的事

回望过去这段路,从最初的“要不要搞”,到现在已经成为日常不可或缺的一部分,中间也经历过迷茫、质疑和失败。但正是那些“怎么都跑不通的测试”、“各种奇怪的兼容性问题”和一次次“重启模拟器”的时刻,才让我意识到:真正的工程实践,从来都不是照搬教程就能搞定的。

如果你也在考虑构建移动应用的自动化测试体系,不妨试着迈出第一步——哪怕只是写一个小测试案例验证某个页面是否存在。当你看到它真正跑起来的那一刻,你会发现:“原来事情真的可以变更好。”

希望这篇文章能为你带来一点启发和参考价值。如果你有任何问题或想要交流更多细节,欢迎留言讨论。

评论 0

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