为啥点了“运行”,我还得手点来回测?——用 DevEco Studio 做自动化 UI 端到端测试(E2E)一把梭

举报
bug菌 发表于 2025/10/27 20:01:03 2025/10/27
【摘要】 🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8 ❓ 前言 🧪🚀写 ArkUI/ArkTS 应用最怕啥?改一行样式、...

🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!

环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

❓ 前言 🧪🚀

写 ArkUI/ArkTS 应用最怕啥?改一行样式、加一个事件,结果某个页面交互“隐性报废”,只有上线后才被用户一顿差评😵。别再“手点回归”内耗啦——这篇手把手用 DevEco Studio 带你搭起 UI 自动化端到端测试

  • ✅ 测试工具配置与工程结构
  • ✅ UI 自动化脚本编写与执行(ArkTS)
  • ✅ 断言与结果校验、失败截图、稳定性“去抖动”
  • ✅ 示例:验证页面交互(输入、点击、滚动、跳转、Toast 校验)

读完你就能把「打开 App → 点击/输入/滑动 → 断言 UI」全自动跑起来,夜里让机器搬砖,白天你专心写更酷的功能😉。

🧭 目录

  • 🧰 一、DevEco Studio 测试工具配置与使用
  • 🧱 二、测试工程结构与关键依赖
  • ✍️ 三、编写 UI 自动化测试:用例、选择器、手势、断言
  • 🧪 四、示例:端到端脚本验证页面交互(可直接粘贴跑)
  • 🧷 五、断言与结果校验:日志、截图、等待策略
  • ⚙️ 六、执行与集成:本地、真机/模拟器、CI 持续集成
  • 🧠 七、稳定性与性能:消除“脆弱测试”的 10 条建议
  • ✅ 八、常用选择器与 API 速查表

🧰 一、DevEco Studio 测试工具配置与使用

1) 开启测试能力

  • 打开 DevEco Studio(下称 DES),File → New → Module,选择你已有应用的 Entry 模块,确保启用了 Test 目录(若模板未生成,手动创建 src/test/ets)。
  • 连接 模拟器真机(推荐真机,UI 延迟与动画更真实)。

2) 安装依赖(ArkTS 测试)

oh-package.json5 中加入常用 UI 测试与断言框架依赖(版本号按你 SDK 对应调整):

{
  "dependencies": {
    "@ohos/hypium": "^1.0.0",     // 测试框架:describe/it/expect
    "@ohos/uitest": "^2.0.0"      // UI 自动化:选择器/驱动/手势
  }
}

@ohos/hypium 提供 BDD 风格用例与断言,@ohos/uitest 提供 UI Driver(查找控件、点击输入、滑动、截图)。

3) 运行入口

  • Run/Debug Configurations 中新增 Module Tests(选择 Entry 模块)。
  • 工具栏选择刚创建的测试配置,点 ▶️ 运行;结果在 RunTests 窗口查看。

🧱 二、测试工程结构与关键依赖

典型结构(仅测试相关):

entry/
  src/
    main/ets/...               # 你的业务代码
    test/ets/                  # UI/E2E 测试
      e2e/
        page-objects/          # 页面对象(可选)
          HomePage.ts
          DetailPage.ts
        utils/
          waiters.ts           # 通用等待/重试
          reporters.ts         # 日志/截图工具
        home.e2e.spec.ets      # 示例:首页交互测试
        detail.e2e.spec.ets    # 示例:详情页交互测试

✍️ 三、编写 UI 自动化测试:用例、选择器、手势、断言

1) 基本模板(Hypium)

import { describe, it, beforeAll, afterAll, expect } from '@ohos/hypium'
import uitest from '@ohos/uitest'

let driver: uitest.UiDriver

describe('E2E: Home interactions', () => {
  beforeAll(async () => {
    driver = await uitest.getUiDriver()  // 连接被测应用
  })

  it('should show initial title', async () => {
    const title = await driver.findComponent(uitest.By.text('首页'))
    expect(!!title).assertTrue()
  })
})

2) 常用选择器

  • By.text('文案'):按可见文本
  • By.id('countText'):按组件 id(确保你在 UI 代码中为关键节点设置 id)
  • By.type('Button'):按类型
  • By.desc('描述'):按无障碍描述(contentDescription)
  • 可链式:By.id('list').hasChild(By.text('标题1'))

3) 手势与输入

await btn.click()
await input.inputText('Hello ArkUI')
await driver.swipe({ fromX: 300, fromY: 1200, toX: 300, toY: 300, duration: 300 })
await driver.scrollTo(By.text('加载更多')) // 列表滚动直达

4) 等待与重试(很关键)

UI 有动画/网络延迟,直接找元素容易“抢早”。建议封装等待工具:

export async function waitFor(driver: uitest.UiDriver, selector: uitest.Selector, timeout=3000) {
  const t0 = Date.now()
  while (Date.now() - t0 < timeout) {
    const node = await driver.findComponent(selector, 200)
    if (node) return node
    await new Promise(r => setTimeout(r, 100))
  }
  throw new Error(`waitFor timeout: ${selector}`)
}

🧪 四、示例:端到端脚本验证页面交互(可直接用)

目标页面(示例):

  • 一个计数器:文本 id 为 countText、按钮文案 +1(点击自增)
  • 一个输入框 id nameInput,按钮 提交,点击后弹出 Toast “已提交:{name}”
  • 列表可滚动到“加载更多”,点击后出现 “已加载更多”

业务页面(示意 ArkTS UI,便于你对照测试脚本设置 id):

// 业务侧示例(片段):请确保关键节点设置 id
@Entry
@Component
struct HomePage {
  @State count: number = 0
  @State name: string = ''
  @State more: boolean = false

  build() {
    Column() {
      Text(`计数:${this.count}`).id('countText').fontSize(20)
      Button('+1').onClick(() => this.count++)

      TextInput({ placeholder: '输入姓名' })
        .id('nameInput')
        .onChange(v => this.name = v)

      Button('提交').onClick(() => {
        prompt.showToast({ message: `已提交:${this.name}` })
      })

      List().width('100%').height(400).id('list') {
        ForEach(Array.from({ length: 30 }, (_, i) => i), (i: number) => ListItem() {
          Text(`条目 ${i}`)
        })
        ListItem() {
          if (!this.more) {
            Button('加载更多').id('loadMoreBtn').onClick(() => this.more = true)
          } else {
            Text('已加载更多').id('loadedTag')
          }
        }
      }
    }.padding(16)
  }
}

测试脚本(E2E)entry/src/test/ets/e2e/home.e2e.spec.ets

import { describe, it, beforeAll, afterAll, expect } from '@ohos/hypium'
import uitest from '@ohos/uitest'

async function waitFor(driver: uitest.UiDriver, selector: uitest.Selector, timeout=4000) {
  const t0 = Date.now()
  while (Date.now() - t0 < timeout) {
    const node = await driver.findComponent(selector, 200)
    if (node) return node
    await new Promise(r => setTimeout(r, 80))
  }
  throw new Error(`waitFor timeout: ${selector}`)
}

let driver: uitest.UiDriver

describe('E2E: Home interactions', () => {
  beforeAll(async () => {
    driver = await uitest.getUiDriver()
  })

  it('计数器点击自增', async () => {
    const label = await waitFor(driver, uitest.By.id('countText'))
    const btn = await waitFor(driver, uitest.By.text('+1'))

    const before = await label.getText()
    await btn.click()
    const after = await label.getText()

    // 断言:数字增加
    const n1 = parseInt(before.replace(/\D+/g, '') || '0', 10)
    const n2 = parseInt(after.replace(/\D+/g, '') || '0', 10)
    expect(n2 > n1).assertTrue()
  })

  it('输入 + 提交后出现 Toast', async () => {
    const input = await waitFor(driver, uitest.By.id('nameInput'))
    await input.inputText('Rebecca')

    const submit = await waitFor(driver, uitest.By.text('提交'))
    await submit.click()

    // Toast 捕获(不同系统版本可能表现不同:用 By.text 配合短等待)
    const toast = await driver.findComponent(uitest.By.text('已提交:Rebecca'), 1500)
    expect(!!toast).assertTrue()
  })

  it('滚动到“加载更多”并验证加载完成状态', async () => {
    const list = await waitFor(driver, uitest.By.id('list'))

    // 滚动到底(多次小步,减少一次性大滑引发的 flake)
    for (let i = 0; i < 4; i++) {
      await driver.scroll(list, { dx: 0, dy: -600, speed: 300 }) // 上滑(坐标系以设备为准)
    }

    const moreBtn = await waitFor(driver, uitest.By.id('loadMoreBtn'))
    await moreBtn.click()

    const loaded = await waitFor(driver, uitest.By.id('loadedTag'))
    expect(await loaded.getText()).assertEqual('已加载更多')
  })

  afterAll(async () => {
    // 失败时可截图留证(示例:无论成功失败都存一张)
    const png = await driver.takeScreenshot()
    // 测试框架通常把文件保存在应用沙箱,可按需要写入固定路径或上传到 CI 附件
    console.info(`screenshot bytes: ${png?.byteLength ?? 0}`)
  })
})

这份脚本涵盖:查找元素 → 点击/输入 → 滚动 → 断言文本/Toast
运行方式:在 DES 选择测试运行配置,点 ▶️,连接模拟器或真机即可。

🧷 五、断言与结果校验:日志、截图、等待策略

  • 断言库(Hypium)

    • expect(x).assertTrue() / assertEqual(a, b) / assertContain(sub, str)
  • 等待策略

    • 固定等待不可取:避免 sleep(2000)。用 waitFor(selector, timeout) 轮询;
    • 交互后用小延时 + 轮询,上限超时即失败,避免卡死;
  • 失败截图

    • driver.takeScreenshot() 获取 ArrayBuffer,保存为文件(便于 CI 附件);
  • 稳定日志

    • 在关键步骤 console.info('step: ...');失败回看 Run 窗口/设备日志(hilog)定位。

⚙️ 六、执行与集成:本地、真机/模拟器、CI

  • 本地运行

    1. 连接设备(USB/同网段模拟器),在 DES 选择测试配置运行;
    2. 多设备并行:建议一次一台,避免抢占/竞争引发 flake;
  • CI 持续集成(思路):

    • 在构建机安装 SDK/CLI,启动 Headless 模拟器 或接入农场设备;
    • 命令行触发测试任务,收集 JUnit/自定义 测试报告 + 截图附件;
    • 失败重跑策略:失败用例重跑 1 次过滤偶发抖动。

🧠 七、稳定性与性能:消除“脆弱测试”的 10 条建议

  1. 为关键控件设置稳定 id,少依赖易变文案;
  2. 所有交互后做等待(轮询 + 超时),别用硬 sleep;
  3. 切小动作:大滑动拆成多次短滑,减少越界;
  4. Toast/弹窗 断言给更宽松时间窗(1–2 秒);
  5. 网络依赖 mock 或固定测试环境,降低不确定性;
  6. 动画影响:可在测试构建里降低时长(配置依项目而定);
  7. 列表/虚拟化:先滚动到可见,再断言;
  8. 并行度:单设备串行执行,避免抢焦点;
  9. 失败立刻截图 + 打点日志
  10. Page Object 模式:把选择器和操作封装成页面对象,减少脚本重复与维护成本。

✅ 八、常用选择器与 API 速查表

能力 用法(示例) 说明
获取驱动 driver = await uitest.getUiDriver() 连接被测应用
找元素 await driver.findComponent(By.id('xxx'), 1000) 支持 By.text/desc/id/type
点击 await node.click() 单击
长按 await node.longClick(800) 毫秒
输入 await node.inputText('Hello') 输入字符
滑动 await driver.swipe({ fromX, fromY, toX, toY, duration }) 屏幕坐标滑动
滚动到 await driver.scrollTo(By.text('目标')) 列表滚动查找
获取文本 await node.getText() 用于断言
截图 await driver.takeScreenshot() 返回二进制数据
等待 await driver.wait(By.id('xxx'), 3000) 某些版本提供内建 wait,可用自封装

实际 API 以你当前 SDK/插件版本为准;命名上可能存在小差异,但能力与思路一致。

🎉 收尾

自动化 UI 测试的价值不在“能不能跑”,而在“稳定、可读、可维护”。用 DevEco Studio + ArkTS 测试栈,把“点击—输入—滚动—断言”固化成脚本,每次提交自动跑一遍,你就能放心地重构 UI、改动交互而不怕“牵一发而动全身”。

反问一句:当回归从一下午缩短到十几分钟,你会把多出来的时间用来修一个老痛点,还是上一个新功能?😉

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」专栏(全网一个名),bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

  最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

  同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板、技术文章Markdown文档等海量资料。

✨️ Who am I?

我是bug菌(全网一个名),CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主/价值贡献奖,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。