华为三折叠手机 Mate XT 发布在即,届时搭载着纯血鸿蒙系统 HarmonyOS NEXT
正式推送给用户使用。该系统不再兼容Android
应用,这就意味着以前 Android 那套自动化测试框架都不能在该系统上运行。在此背景下,hmdriver2
应运而生。
现在我郑重介绍一下hmdriver2
:它是一款支持HarmonyOS NEXT
系统的 UI 自动化框架,无侵入式,提供应用管理,UI 操作,元素定位等功能,轻量高效,上手简单,快速实现鸿蒙应用自动化测试需求。
经过一段时间的调研、编码、实践,就在今天,hmdriver2
正式发布,代码已开源,欢迎提 PR 和 issue,动动你发财的手点点 Star ⭐️(你的 Star 是我迭代的动力)
https://github.com/codematrixer/hmdriver2
uiautomator2
的脚本编写姿势在开发 hmdriver2 前,我对现有的鸿蒙自动化工具链进行了系列调研:
hdc
:类似 android 系统的 adb 命令,提供设备信息查询,包管理,调试相关的命令@ohos.UiTest
:鸿蒙 sdk 的一部分,类似 android sdk 里的uiautomator
,基于 Accessibility 服务,提供模拟 UI 操作的能力。但是需要用arkTS
语言来写自动化 case,并且用例需要打包到被测 app 里面hypium
:鸿蒙官方的自动化框架,功能比较完善,可以基于 python 编写自动化 case分析鸿蒙官方自动化工具链,我们发现hypium
已经具备了实现鸿蒙 Next 系统 UI 自动化能力,那直接用 hypium 就可行,为啥还要重复造轮子再搞一套?
深度体验 hypium 后发现,虽然它提供的能力已经比较全面了,但是还是有一些问题无法满足我的需求:
于是决定自己写一套,说干就干!
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,需要提前将设备注册到开发者账号里,然后重新打包,每次有新设备都要重复出包,想想是不是很难接受。
基于此原因,有了让我继续探索下去的动力,是否有更好的方案?
我的目光再一次来到hypium
身上,为啥它没有依赖 app 也能实现通过 python 调用@ohos.UiTest
的能力,我的猜想是鸿蒙系统肯定给它开了后门(毕竟是自家的),于是我决定阅读下 hypium 的源码(它的代码确实多,包括 4 个基础库xdevice
,xdevice-devicetest
, xdevice-ohos
,hypium
)这里就不详细介绍代码实现了,我找到了里面的关键实现,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
HDC
环境(等价于 android 端的 adb)
hdc
文件在command-line-tools/sdk/HarmonyOS-NEXT-DB2/openharmony/toolchains
目录下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
电脑插上手机,开启 USB 调试,确保执行hdc list targets
可以看到设备序列号
安装hmdirver2
基础库
pip3 install -U hmdriver2
如果需要使用屏幕录屏 功能,则需要安装额外依赖opencv-python
pip3 install -U "hmdriver2[opencv-python]"
// 由于`opencv-python`比较大,因此没有写入到主依赖中,按需安装
接下来就可以愉快的进行脚本开发了 😊😊
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
属性
id
key
text
type
description
clickable
longClickable
scrollable
enabled
focused
selected
checked
checkable
isBefore
isAfter
定位方式包括普通定位,模糊定位,相当定位
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)
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 文档
https://github.com/codematrixer/hmdriver2/tree/master#api-documents