Administrator
发布于 2026-09-02 / 1,231 阅读
0
0

鸿蒙 UI 自动化测试:Hypium 框架与页面对象模式

系列文章《鸿蒙开发从入门到专家》第 46 篇 · 中级

上一篇讲了单元测试。单元测试验证的是"函数对不对",但用户不按函数操作,用户按按钮。一个按钮的 onClick 绑定错了,单元测试不一定能发现。

UI 自动化测试补上这个缺口:模拟用户操作,验证界面行为是否符合预期。鸿蒙的 UI 自动化测试框架叫 Hypium,支持 ArkTS 和 Python 两种语言。这篇以 ArkTS 侧的 @ohos/hypium 为主,讲怎么搭一个可维护的 UI 测试体系。

一、UiTest 框架基础

1.1 核心类

鸿蒙 UI 测试框架基于 UiTest,核心是两个类:

类

职责

UiDriver

驱动设备操作:查找控件、模拟点击/滑动、按键

UiComponent

被找到的控件实例:获取属性、执行操作

注意:早期版本叫 UiDriver,新版 API 12+ 重命名为 Driver,API 有微调。以下代码基于 API 12+ 的 Driver。

1.2 第一个 UI 测试

import { describe, it, beforeAll } from '@ohos/hypium';
import { Driver, ON } from '@kit.TestKit';

export default function uiAbilityTest() {
  describe('LoginPageTest', () => {
    let driver: Driver;

    beforeAll(async () => {
      driver = Driver.create();
      await driver.delayMs(1000);  // 等应用启动完成
    });

    it('should_navigate_to_home_after_login', 0, async () => {
      // 找到用户名输入框并输入
      const usernameInput = await driver.findComponent(ON.id('username_input'));
      await usernameInput.setText('testuser');

      // 找到密码输入框并输入
      const passwordInput = await driver.findComponent(ON.id('password_input'));
      await passwordInput.setText('password123');

      // 点击登录按钮
      const loginButton = await driver.findComponent(ON.text('Login'));
      await loginButton.click();

      // 等待首页加载
      await driver.delayMs(2000);

      // 验证是否到达首页
      const homeTitle = await driver.findComponent(ON.text('Welcome'));
      expect(homeTitle !== null).assertTrue();
    });
  });
}

1.3 控件定位方式

ON(Object Name)是控件定位的入口,提供多种查找策略:

定位方式

API

适用场景

可靠性

ID

ON.id('username')

有唯一 id 的控件

最高

文本

ON.text('Login')

按钮、标签

中(多语言需注意)

类型

ON.type('Button')

找某一类控件

低(结果不唯一)

组合

ON.id('btn').and(ON.text('OK'))

多条件组合

高

尽量用 ON.id 定位。文本定位在多语言环境下会失效,类型定位可能匹配到多个控件。

给关键控件设置 id 是好习惯:

TextInput({ placeholder: 'Username' })
  .id('username_input')  // 测试时用 ON.id('username_input') 定位

Button('Login')
  .id('login_button')

二、页面对象模式

直接在测试用例里写 driver.findComponent(ON.id('xxx')) 会产生大量重复代码。登录页面的控件定位逻辑,5 个测试用例要写 5 遍。改一个 id 要改 5 个文件。

页面对象模式(Page Object Pattern)解决这个问题:把每个页面的控件定位和操作封装成一个类,测试用例调用页面对象的方法。

2.1 定义页面对象

// pages/LoginPage.ts
import { Driver, ON, UiComponent } from '@kit.TestKit';

export class LoginPage {
  constructor(private driver: Driver) {}

  // 定位控件
  async getUsernameInput(): Promise<UiComponent> {
    return await this.driver.findComponent(ON.id('username_input'));
  }

  async getPasswordInput(): Promise<UiComponent> {
    return await this.driver.findComponent(ON.id('password_input'));
  }

  async getLoginButton(): Promise<UiComponent> {
    return await this.driver.findComponent(ON.id('login_button'));
  }

  // 封装操作
  async login(username: string, password: string): Promise<void> {
    const userInput = await this.getUsernameInput();
    await userInput.setText(username);

    const passInput = await this.getPasswordInput();
    await passInput.setText(password);

    const btn = await this.getLoginButton();
    await btn.click();
  }

  // 获取错误提示
  async getErrorMessage(): Promise<string | null> {
    const errorText = await this.driver.findComponent(ON.id('error_message'));
    if (errorText) {
      return await errorText.getText();
    }
    return null;
  }
}

2.2 测试用例使用页面对象

import { LoginPage } from './pages/LoginPage';

export default function uiAbilityTest() {
  describe('LoginPageTest', () => {
    let driver: Driver;

    beforeAll(async () => {
      driver = Driver.create();
      await driver.delayMs(1000);
    });

    it('should_show_error_with_wrong_credentials', 0, async () => {
      const loginPage = new LoginPage(driver);
      await loginPage.login('wronguser', 'wrongpass');
      await driver.delayMs(1000);

      const errorMsg = await loginPage.getErrorMessage();
      expect(errorMsg).assertContain('Invalid');
    });

    it('should_navigate_to_home_with_valid_credentials', 0, async () => {
      const loginPage = new LoginPage(driver);
      await loginPage.login('testuser', 'password123');
      await driver.delayMs(2000);

      const homeTitle = await driver.findComponent(ON.text('Welcome'));
      expect(homeTitle !== null).assertTrue();
    });
  });
}

好处是:页面 UI 变了(比如改了 id),只需要改页面对象类的一个方法,所有测试用例自动适配。

三、常用操作 API

3.1 等待策略

UI 测试最常见的不稳定来源是"控件还没出现就去找"。用固定 delayMs 是最低效的方式,网络快慢波动会导致偶发失败。

driver.waitForComponent 是更好的方式——它会轮询查找,超时才失败:

// 等待控件出现(默认超时 5s)
const button = await driver.waitForComponent(ON.text('Submit'), 5000);

// 等待控件消失
await driver.waitForComponent(ON.text('Loading'), 3000, true);
// 第三参数 true 表示等待消失

3.2 滑动操作

// 向上滑动列表
await driver.swipe({ startX: 300, startY: 800, endX: 300, endY: 200 });

// 向左滑动某个控件
const item = await driver.findComponent(ON.id('list_item_1'));
await item.swipe({ direction: SwipeDirection.LEFT });

3.3 按键模拟

// 返回键
await driver.pressBack();

// 指定 keyCode
await driver.pressKey(KeyCode.KEYCODE_HOME);

// 输入文本
const input = await driver.findComponent(ON.id('search'));
await input.setText('HarmonyOS');

四、测试场景设计

4.1 关键流程覆盖

不需要测试每个页面的每个按钮。优先覆盖:

  1. 核心业务路径:注册 → 登录 → 首页 → 核心功能 → 退出。

  2. 异常路径:网络断开时的行为、权限拒绝时的行为、空数据时的展示。

  3. 跨页面状态:A 页面操作后 B 页面是否正确更新。

4.2 测试数据管理

把测试数据和测试代码分离。用一个 TestData 类集中管理:

// test/TestData.ts
export class TestData {
  static readonly validUser = {
    username: 'testuser',
    password: 'Test1234!',
    email: 'test@example.com'
  };

  static readonly invalidUser = {
    username: 'nonexistent',
    password: 'wrong',
    email: 'invalid-email'
  };

  static readonly longText = 'A'.repeat(500);
  static readonly specialChars = '<script>alert(1)</script>';
}

测试用例中引用:

it('should_reject_sql_injection_in_username', 0, async () => {
  const loginPage = new LoginPage(driver);
  await loginPage.login(TestData.specialChars, 'password');
  const msg = await loginPage.getErrorMessage();
  expect(msg).assertContain('Invalid');
});

4.3 截图与报告

Hypium 支持在测试过程中截图,失败时自动保存:

it('should_display_correct_layout', 0, async () => {
  await driver.delayMs(500);
  await driver.screenCapture('login_page_layout');
  // 截图保存在测试报告目录下
});

五、Python 侧的 Hypium

除了 ArkTS 侧的 @ohos/hypium,华为还提供了 Python 版的 DevEco Testing Hypium。两者定位不同:

维度

ArkTS Hypium

Python Hypium

语言

ArkTS

Python

运行位置

设备内

PC 端,通过 HDC 操作设备

适用场景

随应用打包的 Instrument Test

独立的 UI 回归测试套件

优势

与应用代码同语言,集成度高

可跨应用测试,支持多设备并行

Python Hypium 的典型用法:

from hypium import *

driver = UiDriver.connect(device_sn="DEVICE_ID")

# 启动应用
driver.start_app("com.example.app")

# 授权弹窗处理
driver.touch(BY.key('permission_dialog_allow_button'))

# 输入并点击
username = driver.find_all_components(BY.type("TextInput"))[0]
driver.input_text(username, "testuser")
driver.touch(BY.text('Login'))

# 验证
driver.wait(2)
home_title = driver.find_component(BY.text('Welcome'))
assert home_title is not None

如果你的团队已有 Python 测试基础设施,或者需要跨应用做 UI 回归,Python Hypium 更合适。如果只是给自己的应用做 UI 测试,ArkTS Hypium 更轻量。

小结

要点

建议

控件定位

用 ON.id,不用文本和类型

测试组织

页面对象模式,控件定位和操作封装到类里

等待策略

用 waitForComponent 代替固定 delayMs

测试数据

集中管理,与测试代码分离

框架选择

应用内测试用 ArkTS Hypium,跨应用回归用 Python Hypium

下一篇讲调试技巧。测试发现了 bug,但不知道为什么出 bug——这时候需要调试器上场。


评论