airtest 安装
airtest 常用api
airtest是一个跨平台的、基于图像识别的UI自动化测试框架,适用于游戏和App,支持平台有Windows、Android和iOS
安装airtest
创建python虚拟环境并安装
pip install airtest
安装问题
Could not find a version that satisfies the requirement opencv-contrib-python<=3.4.2.17 (from airtest) (from versions: 3.4.8.29, 4.1.2.30...)。
这是因为Airtest 使用了 opencv-contrib-python 作为依赖且版本需要 <=3.4.2.17,而opencv-contrib-python-3.4.2.17 不支持 python 3.8及以上版本。在官方未提供支持前,解决方案是创建<=python3.7的虚拟环境
添加airtest使用的adb的执行权限
permissionError: [Errno 13] Permission denied: '~/.pyenv/versions/3.6.8/lib/python3.6/site-packages/airtest/core/android/static/adb/mac/adb'
airtest使用的是自身的adb,在安装包的如上目录中,在Mac/Linux系统下,需要手动赋予该adb可执行权限,否则可能在执行脚本时遇到 Permission denied.
chmod +x adb
- 其他安装方面的问题可参看python运行环境部署
官方文档
常用api及示例
以下以android为例
device相关操作
- get_default_device:获取默认 device
- uuid:获取当前 Device 的 UUID
- list_app:列举所有 App
- path_app:打印输出某个 App 的完整路径
- check_app:检查某个 App 是否在当前设备上
- start_app:启动某个 App
- start_app_timing:启动某个 App,然后计算时间
- stop_app:停止某个 App
- clear_app:清空某个 App 的全部数据
- install_app:安装某个 App
- install_multiple_app:安装多个 App
- uninstall_app:卸载某个 App
- snapshot:屏幕截图
- shell:获取 Adb Shell 执行的结果
- keyevent:执行键盘操作
- wake:唤醒当前设备
- home:点击 HOME 键
- text:向设备输入内容
- touch:点击屏幕某处的位置
- double_click:双击屏幕某处的位置
- swipe:滑动屏幕,由一点到另外一点
- pinch:手指捏和操作
- logcat:日志记录操作
- getprop:获取某个特定属性的值
- get_ip_address:获取 IP 地址
- get_top_activity:获取当前 Activity
- get_top_activity_name_and_pid:获取当前 Activity 的名称和进程号
- get_top_activity_name:获取当前 Activity 的名称
- is_keyboard_shown:判断当前键盘是否出现了
- is_locked:设备是否锁定了
- unlock:解锁设备
- display_info:获取当前显示信息,如屏幕宽高等
- get_display_info:同 display_info
- get_current_resolution:获取当前设备分辨率
- get_render_resolution:获取当前渲染分辨率
- start_recording:开始录制
- stop_recording:结束录制
- adjust_all_screen:调整屏幕适配分辨率
from airtest.core.android import Android
from airtest.core.api import *
import logging
logging.getLogger("airtest").setLevel(logging.WARNING)
device: Android = init_device('Android')
ip_address = device.get_ip_address()
print(0, f'ip_address: {ip_address}')
is_locked = device.is_locked()
print(1, f'is_locked: {is_locked}')
if is_locked:
device.unlock()
device.wake()
default_device = device.get_default_device()
print(2, f'default_device: {default_device}')
uuid = device.uuid
print(3, f'uuid: {uuid}')
app_list = device.list_app()
print(4, f'app list: {app_list}')
# app_path = device.path_app(app_list[0])
# print(5, f'app path: {app_path}')
# display_info: dict = device.display_info
display_info: dict = device.get_display_info()
print(5, f'display info: {display_info}',
f'width and height: {display_info.get("width")}, {display_info.get("height")}')
current_resolution = device.get_current_resolution()
print(6, f'current_resolution: {current_resolution}')
render_resolution = device.get_render_resolution()
print(7, f'render_resolution: {render_resolution}')
is_keyboard_shown = device.is_keyboard_shown()
print(8, f'is keyboard shown: {is_keyboard_shown}')
package_name = 'com.android.dazhihui'
apk_path = 'test.apk'
is_app_installed = False
try:
is_app_installed = device.check_app(package_name)
except:
pass
if is_app_installed:
print(9, is_app_installed)
print(10, f'app path: {device.path_app(package_name)}')
# device.clear_app(package_name)
device.stop_app(package_name)
device.start_app(package_name)
time.sleep(5)
top_activity = device.get_top_activity()
top_activity_name_and_pid = device.get_top_activity_name_and_pid()
top_activity_name = device.get_top_activity_name()
print(11, f'top activity related: {top_activity}, {top_activity_name_and_pid}, {top_activity_name}')
# device.uninstall_app(package_name)
else:
device.install_app(apk_path, replace=True)
device.home()
current_resolution_width = current_resolution[0]
current_resolution_height = current_resolution[1]
device.swipe((current_resolution_width/5, current_resolution_height/2), (current_resolution_width, current_resolution_height/2))
touch(Template('seach.png')
text("wechat",enter=False,search=True) # 默认情况下,text是带回车的,不需要可以传入False;安卓平台下,还支持输入后点击软键盘的搜索按钮
对install_app的补充说明
install_app(filepath,replace=False, install_options=None)
- filepath:apk文件在PC上的完整路径
- replace:如果应用已存在,是否替换,默认为False
- install_options:install 命令的额外选项,默认是None,可填入 "-l"、"-t"、"-s"、"-d"和"-g" 等参数,用于控制安装apk的行为。多个参数时,可用list传入,如
['-d', '-g']
- 其中,install_options 的各参数的含义如下:
- "-l" ,将应用安装到保护目录/mnt/asec
- "-t" ,允许安装AndroidManifest.xml里application指定android:testOnly="true"的应用
- "-s" ,将应用安装到sdcard
- "-d" ,允许降级覆盖安装
- "-g" ,授予所有运行时权限
device处理
-
初始化设备
如果设备没有被初始化的话会进行初始化,并把初始化的设备作为当前设备,源码如下def init_device(platform="Android", uuid=None, **kwargs): """ Initialize device if not yet, and set as current device. :param platform: Android, IOS or Windows :param uuid: uuid for target device, e.g. serialno for Android, handle for Windows, uuid for iOS :param kwargs: Optional platform specific keyword args, e.g. `cap_method=JAVACAP` for Android :return: device instance """ cls = import_device_cls(platform) dev = cls(uuid, **kwargs) # Add device instance in G and set as current device. G.add_device(dev) return dev
-
获取当前设备
from airtest.core.android import Android from airtest.core.api import * import logging logging.getLogger("airtest").setLevel(logging.WARNING) device_init: Android = init_device('Android') print(device_init) print(device()) # 获取当前 device
可以发现它返回的是一个 Android 对象。 这个 Android 对象实际上属于 airtest.core.android 这个包,继承自 airtest.core.device.Device 这个类,与之并列的还有 airtest.core.ios.ios.IOS 、airtest.core.linux.linux.Linux、airtest.core.win.win.Windows 等
-
关于设备字符串
以安卓为例, 字串完整定义如下:Android://<adbhost[localhost]>:<adbport[5037]>/<serialno>
adbhost是adb server所在主机的ip,默认是本机127.0.0.1,adb port默认是5037,serialno是android手机的序列号
# 什么都不填写,会默认取当前连接中的第一台手机 Android:/// # 连接本机默认端口连的一台设备号为79d03fa的手机 Android://127.0.0.1:5037/79d03fa # 用本机的adb连接一台adb connect过的远程设备,注意10.254.60.1:5555其实是serialno Android://127.0.0.1:5037/10.254.60.1:5555 # 连接一个Windows窗口,窗口句柄为123456 Windows:///123456 # 连接一个Windows窗口,窗口名称匹配某个正则表达式 Windows:///?title_re=Unity.* # 连接windows桌面,不指定任何窗口 Windows:/// # 连接iOS手机 iOS:///127.0.0.1:8100
-
连接设备
源码如下:(其实内部就是初始化设备init_device,返回设备对象)def connect_device(uri): """ Initialize device with uri, and set as current device. :param uri: an URI where to connect to device, e.g. `android://adbhost:adbport/serialno?param=value¶m2=value2` :return: device instance :Example: * ``android:///`` # local adb device using default params * ``android://adbhost:adbport/1234566?cap_method=javacap&touch_method=adb`` # remote device using custom params * ``windows:///`` # local Windows application * ``ios:///`` # iOS device """ d = urlparse(uri) platform = d.scheme host = d.netloc uuid = d.path.lstrip("/") params = dict(parse_qsl(d.query)) if host: params["host"] = host.split(":") dev = init_device(platform, uuid, **params) # 直接连接,其实内部也进行了初始化 return dev
举例:
from airtest.core.android import Android from airtest.core.api import * import logging logging.getLogger("airtest").setLevel(logging.WARNING) uri = 'Android://127.0.0.1:5037/9A201FFAZ0007D' device: Android = connect_device(uri) print(device)
-
获取所有设备
在 airtest 中有一个全局类 G,类属性DEVICE_LIST用于存储所有连接的设备from airtest.core.android import Android from airtest.core.api import * import logging logging.getLogger("airtest").setLevel(logging.WARNING) uuid = ['9A201FFAZ0007D', 'FJH5T18A10012174'] for i in uuid: connect_device(f'Android://127.0.0.1:5037/{i}') print(G.DEVICE_LIST)
最开始没有调用 connect_device 方法之前,DEVICE_LIST 是空的,在调用之后 DEVICE_LIST 会自动添加已经连接的 device,DEVICE_LIST 就是已经连接的 device 列表
-
切换设备
from airtest.core.android import Android from airtest.core.api import * import logging logging.getLogger("airtest").setLevel(logging.WARNING) uuid = ['9A201FFAZ0007D', 'FJH5T18A10012174'] for i in uuid: connect_device(f'Android://127.0.0.1:5037/{i}') print(G.DEVICE_LIST) print(device()) # 默认是最后一个 set_current(uuid[0]) # idx: uuid or index of initialized device instance print(device())
在设备上执行命令
shell源码如下:
@logwrap
def shell(cmd):
"""
Start remote shell in the target device and execute the command
:param cmd: command to be run on device, e.g. "ls /data/local/tmp"
:return: the output of the shell cmd
:platforms: Android
"""
return G.DEVICE.shell(cmd)
示例(获取内存信息就可以使用如下命令):
from airtest.core.api import *
import logging
logging.getLogger("airtest").setLevel(logging.WARNING)
uri = 'Android://127.0.0.1:5037/9A201FFAZ0007D'
connect_device(uri)
result = shell('cat /proc/meminfo')
print(result)
其他操作
上面举例了swipe和home操作,这里再补充介绍下snapshot,touch,pinch,keyevent
snapshot(filename=None, msg="", quality=None, max_size=None)
from airtest.core.api import * uri = 'Android://127.0.0.1:5037/emulator-5554' connect_device(uri) package = 'com.tencent.mm' start_app(package) sleep(3) snapshot('weixin.png', quality=30)
touch(v, times=1, **kwargs)
/click
、double_click(v)
点击,可以传入一张图或者绝对位置,同时可以指定点击次数。touch 方法完全等同于 click 方法。如果要双击的话,还可以使用调用 double_click 方法,传入参数也可以是 Template 或者绝对位置keyevent(keyname, **kwargs)
keyevent 来输入某个键,例如 home、back 等等pinch(in_or_out='in', center=None, percent=0.5)
放大缩小是使用的 pinch 方法,可以指定放大还是缩小,同时还可以指定中心位置点和放大缩小的比率- 示例
from airtest.core.api import * import logging logging.getLogger("airtest").setLevel(logging.WARNING) uri = 'Android://127.0.0.1:5037/9A201FFAZ0007D' connect_device(uri) # touch(Template('tpl.png')) click(Template('tpl.png')) home() touch((400, 600), times=2) keyevent('HOME') double_click((400, 600)) keyevent('BACK') keyevent('BACK') pinch(in_or_out='out', center=(300, 300), percent=0.4)
- 其他
根据实际情况查看源码调用即可,这里不再举例说明,可参看1个模拟各种复杂的滑动或手势操作的方法- device.two_finger_swipe()
- device.swipe_along()
- device.maxtouc()
- device.minitouch()
- device.touch_proxy()
等待和判断
- 可以使用 wait 方法等待某个内容加载出来,需要传入的是 Template,返回coordinates of the matched target。常用于等待某一张图片出来之后,再进行下一步操作,可以传入等待的超时时长、查找的时间间隔和首次尝试查找失败的回调函数
- 也可以使用 exists 方法判断某个内容是否存在
from airtest.core.api import *
import logging
logging.getLogger("airtest").setLevel(logging.WARNING)
def test():
print("未等待到目标")
uri = 'Android://127.0.0.1:5037/9A201FFAZ0007D'
device = connect_device(uri)
resolution = device.get_current_resolution()
if not exists(Template('tpl.png')):
swipe((resolution[0]/2, resolution[1]*3/4), (resolution[0]/2, resolution[1]/4))
else:
touch(Template('tpl.png')
wait(Template(r"tpl.png", record_pos=(-0.036, -0.189), resolution=(1080, 1920)),timeout=120,interval=3,intervalfunc=test())
查找目标find_all()和loop_find()
- find_all()在设备屏幕上查找所有出现的目标并返回其坐标列表
- loop_find()循环查找,查看源码可发现touch()、wait()和exists()内部都调用了loop_find()
断言
- assert_exists 、assert_not_exists、assert_equal、assert_not_equal
- assert_exists和assert_not_exists,判断某个目标是否存在于屏幕上,同时还可以传入 msg,它可以被记录到 report 里面
查看源码查找目标loop_find(v, timeout=ST.FIND_TIMEOUT, threshold=ST.THRESHOLD_STRICT or v.threshold),超时时间和阙值都是全局变量,timeout为20s,threasold为0.7,所以脚本只有在20s内找到置信度>0.7的结果,断言才会成功
- assert_equal和assert_not_equal,判断传入的两个值是否相等,同时还可以传入 msg,它可以被记录到 report 里面
assert_exists(Template('tpl.png'), msg='该目标存在')
# assert_equal("实际值", "预测值", "目标值正确"),以下代码中value通过控件属性获取
assert_equal(value, "8", "按钮值为8")