前言

华为三折叠手机 Mate XT 发布在即,届时搭载着纯血鸿蒙系统 HarmonyOS NEXT 正式推送给用户使用。该系统不再兼容Android应用,这就意味着以前 Android 那套自动化测试框架都不能在该系统上运行。在此背景下,hmdriver2应运而生。

现在我郑重介绍一下hmdriver2:它是一款支持HarmonyOS NEXT系统的 UI 自动化框架,无侵入式,提供应用管理,UI 操作,元素定位等功能,轻量高效,上手简单,快速实现鸿蒙应用自动化测试需求。

经过一段时间的调研、编码、实践,就在今天,hmdriver2正式发布,代码已开源,欢迎提 PR 和 issue,动动你发财的手点点 Star ⭐️(你的 Star 是我迭代的动力)

https://github.com/codematrixer/hmdriver2

设计思想

方案探索

在开发 hmdriver2 前,我对现有的鸿蒙自动化工具链进行了系列调研:

为啥不用 hypium?

分析鸿蒙官方自动化工具链,我们发现hypium已经具备了实现鸿蒙 Next 系统 UI 自动化能力,那直接用 hypium 就可行,为啥还要重复造轮子再搞一套?

深度体验 hypium 后发现,虽然它提供的能力已经比较全面了,但是还是有一些问题无法满足我的需求:

  1. 安装的依赖多,使用较繁杂,对用户不够友好,特别是小白用户
  2. 脚本执行效率较低,操作响应慢
  3. 未正式开源,无法共建,框架有 bug,等待官方修复时间确实太长。

于是决定自己写一套,说干就干!

1.0 版本

1.0 版本参考了 Android 端的 uiautomator2 自动化框架(这是一个很优秀的框架,不管是它的设计思路还是代码编写都值得借鉴),它的原理是在手机上运行了一个 http rpc 服务,将 android sdk 的uiautomator中的功能开放出来,然后再将这些 http 接口封装成 python 库。

类比到鸿蒙NEXT系统,前面说的@ohos.UiTest等价于 Android 系统的uiautomator,那只需要要手机端也实现一个 testRunner APP,这个 app 提供一个 socket 服务将鸿蒙系统@ohos.UiTest的 UI 操作的能力通过接口暴露出来,最终实现 python 调用。

思路有了,于是开始实现了,一顿操作猛于虎,几天过去了,demo 快亮相时,突然发现 github 上已经有人用相同的思路实现了,它的名字叫hmdriver,还是有人比我快啊(这也是我的框架为啥叫hmdriver2,这是后话)

这个版本需要单独开发一个 testRunner APP,然后将 app 安装到手机上,可以理解为侵入式,不够轻量,不符合即插即用原则;而且鸿蒙 Next 系统打包签名机制比较繁琐(类似于苹果的开发者证书那一套,天下苦苹果久矣),那时候还没有企业证书,我们的测试设备要想安装上 testRunner APP,需要提前将设备注册到开发者账号里,然后重新打包,每次有新设备都要重复出包,想想是不是很难接受。

基于此原因,有了让我继续探索下去的动力,是否有更好的方案?

2.0 版本(hmdriver2)

我的目光再一次来到hypium身上,为啥它没有依赖 app 也能实现通过 python 调用@ohos.UiTest的能力,我的猜想是鸿蒙系统肯定给它开了后门(毕竟是自家的),于是我决定阅读下 hypium 的源码(它的代码确实多,包括 4 个基础库xdevicexdevice-devicetestxdevice-ohoshypium)这里就不详细介绍代码实现了,我找到了里面的关键实现,hypium是通过一系列 socket 调用和手机通信(端口是8012),这个 socket 服务是通过hdc命令启动uitest实现的

hdc shell uitest start-daemon singleness

同时hypium提供了一个动态链路库agent.so,这个是核心中的核心,所有的 UI 操作,录屏等功能都在这个动态库里实现,它可以在hdc uitest命令运行时被加载到内存中,然后其功能可以被uitest进程调用(IPC)

找到这个关键步骤后,我们可以把agent.so文件拿到,然后自己实现一套调用逻辑,这样就不用依赖 testRunner app 了

几个昼夜过去了,我整理出了详细的调用协议(详情请看 DEVELOP.md),然后快速开发出了 python 基础库,它的名字叫 hmdriver2

快速上手

  1. 配置鸿蒙HDC环境(等价于 android 端的 adb)
    1. 下载 Command Line Tools 并解压
    2. hdc文件在command-line-tools/sdk/HarmonyOS-NEXT-DB2/openharmony/toolchains目录下
    3. 配置环境变量,macOS 为例,在~/.bash_profile 或者 ~/.zshrc 文件中添加
export HM_SDK_HOME="/Users/develop/command-line-tools/sdk/HarmonyOS-NEXT-DB2"  //请以sdk实际安装目录为准
export PATH=$PATH:$HM_SDK_HOME/hms/toolchains:$HM_SDK_HOME/openharmony/toolchains
export HDC_SERVER_PORT=7035
  1. 电脑插上手机,开启 USB 调试,确保执行hdc list targets 可以看到设备序列号

  2. 安装hmdirver2 基础库

    pip3 install -U hmdriver2
    

    如果需要使用屏幕录屏 功能,则需要安装额外依赖opencv-python

    pip3 install -U "hmdriver2[opencv-python]"
    // 由于`opencv-python`比较大,因此没有写入到主依赖中,按需安装
    
  3. 接下来就可以愉快的进行脚本开发了 😊😊

    from hmdriver2.driver import Driver
    d = Driver("FMR0223C13000649")
    print(d.device_info)
    # ouput: DeviceInfo(productName='HUAWEI Mate 60 Pro', model='ALN-AL00', sdkVersion='12', sysVersion='ALN-AL00 5.0.0.60(SP12DEVC00E61R4P9log)', cpuAbi='arm64-v8a', wlanIp='172.31.125.111', displaySize=(1260, 2720), displayRotation=<DisplayRotation.ROTATION_0: 0>)
    d.start_app("com.kuaishou.hmapp", "EntryAbility")
    d(text="精选").click()
    ...
    

功能介绍

应用管理

代码示例

from hmdriver2.driver import Driver

d = Driver("FMR0223C13000649")  # 替换成你的serial

d.install_app("/Users/develop/harmony_prj/demo.hap")

d.start_app("com.kuaishou.hmapp", "EntryAbility")

d.uninstall_app("com.kuaishou.hmapp")

d.stop_app("com.kuaishou.hmapp")

# 该方法表示清除App数据和缓存
d.clear_app("com.kuaishou.hmapp")

设备操作

代码示例

from hmdriver2.proto import DeviceInfo

info: DeviceInfo = d.device_info
# output:
# DeviceInfo(productName='HUAWEI Mate 60 Pro', model='ALN-AL00', sdkVersion='12', sysVersion='ALN-AL00 5.0.0.60(SP12DEVC00E61R4P9log)', cpuAbi='arm64-v8a', wlanIp='172.31.125.111', displaySize=(1260, 2720), displayRotation=<DisplayRotation.ROTATION_0: 0>)

# 获取设备旋转状态
rotation = d.display_rotation
# ouput: DisplayRotation.ROTATION_0

# KeyEvent
d.press_key(KeyCode.POWER)

# 截图
d.screenshot(""./test.png")

手势操作包括单击,双击,滑动,输入,复杂手势

d.click(200, 300)
d.click(0.4, 0.6)
d.double_click(500, 1000)
d.swipe(0.5, 0.8, 0.5, 0.4, speed=2000)

d.gesture.start(x1, y1, interval=.5).move(x2, y2).pause(interval=1).move(x3, y3).action()

如下是一个复杂手势的效果展示

屏幕录屏

with d.screenrecord.start("test2.mp4"):
    # do somethings
    time.sleep(5)

通过上下文语法,在录屏结束时框架会自动调用stop 清理资源

控件操作

控件查找支持这些by属性

定位方式包括普通定位,模糊定位,相当定位

d(text="tab_recrod")

d(id="drag")

# 定位所有`type`为Button的元素,选中第0个
d(type="Button", index=0)

# 定位`type`为Button且`text`为tab_recrod的元素
d(type="Button", text="tab_recrod")

# 定位`text`为showToast的元素的前面一个元素
d(text="showToast", isAfter=True) 

# 定位`id`为drag的元素的后面一个元素
d(id="drag", isBefore=True)

定位到控件后就可以进行信息获取和控件操作了

from hmdriver2.proto import ComponentData

d(text="tab_recrod").info

# output:
{
    "id": "",
    "key": "",
    "type": "Button",
    "text": "tab_recrod",
    "description": "",
    "isSelected": False,
    "isChecked": False,
    "isEnabled": True,
    "isFocused": False,
    "isCheckable": False,
    "isClickable": True,
    "isLongClickable": False,
    "isScrollable": False,
    "bounds": {
        "left": 539,
        "top": 1282,
        "right": 832,
        "bottom": 1412
    },
    "boundsCenter": {
        "x": 685,
        "y": 1347
    }
}

d(text="tab_recrod").click()
d(type="Button", text="tab_recrod").click()

d(text="tab_recrod").click_if_exists()

d(text="tab_recrod").double_click()
d(text="tab_recrod").long_click()

# 控件拖拽
componentB: ComponentData = d(type="ListItem", index=1).find_component()
d(type="ListItem").drag_to(componentB)  # 将元素拖动到元素B上

# 控件缩放
d(text="tab_recrod").pinch_in(scale=0.5)
d(text="tab_recrod").pinch_out(scale=2)

Toast 获取

hmdriver2 还可以获取界面的 toast,用法如下

# 启动toast监控
d.toast_watcher.start()

# do something 比如触发toast的操作
d(text="xx").click()  

# 获取toast
toast = d.toast_watcher.get_toast()

# output: 'testMessage'

PS. 所有功能详细介绍可以查看 API 文档

API 文档

https://github.com/codematrixer/hmdriver2/tree/master#api-documents

后续计划


↙↙↙阅读原文可查看相关链接,并与作者交流