一、引言:为什么选择 Airtest+Poco

1.1、工具定位与优势

Airtest:基于图像识别的跨平台自动化框架(支持 Android/iOS/Windows),适合动态 UI 场景(如游戏、不规则按钮)。
Poco:基于UI控件识别的定位工具,通过控件属性定位,适合静态 UI(如原生 APP、Unity/Cocos 游戏),它本质上也是 python 第三方库。

组合价值:互补覆盖 90%+自动化场景,支持低代码开发(AirtestIDE),降低脚本维护成本。

注意:因为Airtest和poco都是基于python语法的,所以我们编写的测试脚本都应该符合python语法。

1.2、适用场景

  1. 移动 APP 自动化测试(功能测试、回归测试)。
  2. 游戏自动化(登录、任务流程、兼容性测试)。
  3. 跨设备批量操作(如多机型兼容性验证)。

1.3、前置准备

环境搭建:AirtestIDE 安装、Python依赖(pip install airtest pocoui)。
设备连接:Android(USB 调试)、iOS(证书配置)、模拟器设置。

1.3.1、连接Android手机

通过ADB连接你的电脑和Android手机,即可开始调试Android应用。ADB是Google官方提供的Android调试工具。AirtestIDE依赖ADB与安卓设备进行通信。

打开AirtestIDE,按照以下步骤进行连接:

  1. 打开手机设置-开发者选项-USB调试开关;
  2. 在AirtestIDE设备面板中点击refresh ADB按钮,查看连接上的设备;
  3. 如果没有显示出设备,试试restart ADB;
  4. 能够成功看到设备后,点击对应设备的Connect按钮,进行初始化。

查看设备是否连接成功

adb devices

"""
C:\Users\admin>adb devices
List of devices attached
XCWKJR*****PCUC6        device
"""

在Airtest IDE中连结

1.3.2、连接iOS手机

要连接一台iOS手机,你需要先准备好一台安装了Xcode的Mac电脑,连接方法参考文档。

Airtest教程文档:https://airtest.doc.io.netease.com/IDEdocs/3.2device_connection/4_ios_connection/

1.3.3、连接Windows窗口

对于Windows桌面程序的测试,AirtestIDE可以将被测窗口嵌入,方便脚本录制和调试。

  1. 在AirtestIDE设备面板中点击选择游戏画面按钮;
  2. 将鼠标移动到被测程序的窗口上,会显示红色边框,将对应的窗口框出;
  3. 单击鼠标左键即可将对应的窗口嵌入到AirtestIDE中;
  4. 也可以点击搜索窗口按钮,选择对应title的窗口,然后点击连接。

选择游戏画面

若选择了windows出口无嵌入连结,右方显示这个样即为连接成功

二、Airtest图像识别核心方法

2.1、设备控制与环境初始化

2.1.1、初始化环境

通过auto_setup()初始化连接(脚本常用,自动连接)。

auto_setup(__file__, devices=["设备连接字符串"], logdir="./log")

devices:设备连接字符串列表(支持多设备,按顺序连接)
logdir:日志存储路径(可选)

源码

auto_setup(basedir=None, devices=None, logdir=None, project_root=None, compress=None)

"""
自动配置运行环境,如果当前没有连接设备的话,就默认尝试连接Android设备。
参数:
basedir – 设置当前脚本的所在路径,也可以直接传 __file__ 变量进来
devices – 一个内容为 connect_device uri 字符串的列表
logdir – 可设置脚本运行时的log保存路径,默认值为None则不保存log,如果设置为True则自动保存在<basedir>/log目录中。
project_root – 用于设置PROJECT_ROOT变量,方便 using 接口的调用
compress – 屏幕截图的压缩比率,在[1, 99]范围内的整数,默认是10
"""

案例

# 1. 连接单个Android设备(通过设备ID)
auto_setup(__file__, devices=["Android://127.0.0.1:5037/ABC123456"])  # ABC123456为设备ID(通过adb devices获取)

auto_setup(__file__, devices=["Android://127.0.0.1:5037/SJE5T17B17"], logdir=True, project_root=r"D:\test\logs", compress=90)

# 2. 连接模拟器(指定端口)
auto_setup(__file__, devices=["Android://127.0.0.1:5037/127.0.0.1:62001"])  # 62001为模拟器端口

通过connect_device()手动连接(灵活控制,适合多设备切换)。
connect_device()函数用于在脚本运行中动态连接设备,支持随时切换设备(配合set_current()),适合多设备协同操作的场景。

源码

connect_device(uri)

"""
用URI字符串来初始化设备,并且设置为当前设备。
参数:uri一个用于初始化设备的URI字符串,例如android://adbhost:adbport/serialno?param=value&param2=value2
return:device对象
"""

案例

dev = connect_device("设备连接字符串")  # 返回设备对象
set_current(dev)  # 切换到当前设备(若连接多个设备)
from airtest.core.api import *

# 1. 连接Android设备
android_dev = connect_device("Android://127.0.0.1:5037/ABC123456")
# 2. 连接Windows窗口
windows_dev = connect_device("Windows:///?title=Unity")

# 切换到Android设备操作
set_current(android_dev)
touch(Template("android_btn.png"))

# 切换到Windows窗口操作
set_current(windows_dev)
keyevent("f")  # 按F键

使用connect_device("Android:///")连接,可以省去序列号,然后返回设备信息

def get_device_serial():
    """在Airtest中获取已连接设备的序列号"""
    try:
        # 连接设备(如果未连接,需先执行)
        # 格式:Android:///序列号(若只连一个设备,可简化为Android:///)
        connect_device("Android:///")

        # 获取设备对象并返回序列号
        dev = device()
        return dev.serialno  # Android设备的序列号属性
    except Exception as e:
        print(f"获取设备序列号失败:{e}")
        return None

关键:设备连接字符串格式
不同设备的连接字符串格式不同,这是连接成功的核心,汇总如下:

设备类型连接字符串格式示例
Android 设备Android://adb_server_ip:adb_server_port/设备IDAndroid://127.0.0.1:5037/ABC123456
Android 模拟器Android://127.0.0.1:5037/模拟器端口(模拟器 ADB 端口,如夜神 62001)Android://127.0.0.1:5037/127.0.0.1:62001
iOS 设备iOS:///设备IP:端口(默认端口8100,需先启动ios-tagent)iOS:///192.168.1.100:8100
Windows 窗口Windows:///窗口句柄或Windows:///?title=窗口标题(标题支持模糊匹配)Windows:///?title=Unity或Windows:///12345(12345 为句柄)
# 使用默认参数连接本地ADB设备,当只有一个设备时使用
connect_device("Android:///") 
# 本地设备,序列号SJE5T17B17,含自定义参数
connect_device("Android:///SJE5T17B17?cap_method=javacap&touch_method=adb")
# 使用自定义参数的远程设备 Android://adbhost:adbport/serialno
connect_device("Android://127.0.0.1:5037/10.254.60.1:5555")
# 将seriano添加到参数中
connect_device("Android://127.0.0.1:5037/10.234.60.1:5555?name=serialnumber")

# 连接到桌面
connect_device("Windows:///") 
# 用句柄123456连接到窗口
connect_device("Windows:///123456")
# 连接到名称包含“explorer”的窗口
connect_device("windows:///?title_re='.*explorer.*'")
# 连接到窗口而不将其设置为前台
connect_device("Windows:///123456?foreground=False")

# iOS设备
connect_device("iOS:///127.0.0.1:8100")
# 带有mjpeg端口的iOS
connect_device("iOS:///http://localhost:8100/?mjpeg_port=9100")
# 带有mjpeg端口和udid的iOS
connect_device("iOS:///http://localhost:8100/?mjpeg_port=9100&&udid=00008020-001270842E88002E")
# udid/uuid/serialno都正常
connect_device("iOS:///http://localhost:8100/?mjpeg_port=9100&&uuid=00008020-001270842E88002E")

2.1.2、应用管理

2.1.2.1、获取app的版本号

通过下面两条命令就可以获得app的版本号:

  1. 版本是1.00,则输出版本号为100。
  2. 第一条命令中的APK_PATH,传入的是PC上apk的路径。
  3. 第二条命令中的PACKAGE,传入是的包名。
# 获取本地apk文件的版本号
apk_version = int(APK(APK_PATH).androidversion_code)

# 获取手机已安装包体的版本号
installed_version = android.adb.get_package_version(PACKAGE)

检测是否需要执行覆盖安装的思路大概是:

  1. 获取本地apk的版本号(即准备安装到手机上的那个包体) ;
  2. 获取手机里已经安装好的包体的版本号;
  3. 对比获得的2个版本号,如果本地apk版本号大于手机里包体的版本号,则判断需要执行覆盖安装。

关于“安装包体”的几个小知识点:

  1. 检查package在设备中是否存在:check_app(package)
  2. 打印出package的完整路径:path_app(package)
  3. 获取本地apk文件的版本号:int(APK(apk_path).androidversion_code)
  4. 获取手机中包体的版本号:Android().adb.get_package_version(package)
  5. 覆盖安装:install(r"D:\test.apk", install_options=["-r", "-t"])
2.1.2.2、安装app

Airtest中的install()

# 安装Android apk
install(r"D:\demo\test.apk")
# adb install -r -t D:\\demo\\test.apk
install(r"D:\demo\test.apk", install_options=["-r", "-t"])

# 安装iOS ipa
install(r"D:\demo\test.ipa")
# 通过url安装iOS ipa
install("http://www.example.com/test.ipa")
install_options说明
-t允许测试包
-l锁定应用程序
-s把应用程序安装到sd卡上
-g为应用程序授予所有运行时的权限
-r替换已存在的应用程序(覆盖安装)
-d允许降级覆盖安装

Android中的install_app()

from airtest.core.android.android import *
Android().install_app(filepath, replace=False, install_options=None)
  1. filepath:apk文件在PC上的完整路径。
  2. replace: 如果应用已存在,是否替换,默认为False。
  3. install_options:install 命令的额外选项,默认是[],可填入 "-l"、"-t"、"-s"、"-d"和"-g" 等参数,用于控制安装apk的行为。
2.1.2.3、启动app
start_app(package, activity=None)
  • 启动应用。
  • 参数:
  1. package – 包名。
  2. activity – 界面名。
2.1.4.4、启动app,返回启动时间

源码

start_app_timing(package, activity)
  • 启动应用,并且返回启动耗费时间
  • 参数:
  1. package – 包名。
  2. activity – 界面名。
  • 返回:app启动时间。

案例

# 获取设备当前页面的界面名
name =  device().get_top_activity_name()

# 获取启动时间
time = device().start_app_timing(
	package="park.master.car.parking.games",  # 启动的包名
	activity="com.common.common.act.v2.BaseAgentAct"  # 需要进入的界面名
)
print(time)  # 打印启动时间,单位ms
2.1.2.5、关闭app
# 关闭指定 App
stop_app(package)	
2.1.2.6、清除app数据
# 清除 App 数据(重置环境)
clear_app(package) 

2.1.3、系统操作

2.1.3.1、模拟设备按键 - keyevent()

安卓设备的按键码
在脚本中,有时需要输入一些指定的按键,例如点一下HOME键、BACK键等,如果设备是Android设备,可以参考谷歌的 Android按键码。

安卓键码表:https://developer.android.google.cn/reference/kotlin/android/view/KeyEvent.html

keyevent("BACK")  # 返回键)
keyevent("HOME")  # Home 键
keyevent("KEYCODE_DEL")  # 等同于keyevent("67")

Windows平台的按键码
在Windows系统中,请参考pywinauto这个库提供的Windows按键码。

https://pywinauto.readthedocs.io/en/latest/code/pywinauto.keyboard.html

keyevent("{DEL}")
keyevent("{BACKSPACE}")

iOS设备的按键码
iOS设备现在暂时只支持HOME按键的keyevent 。

keyevent("HOME")

注意:在Windows平台测试PC游戏,或者使用Airtest连接Unity编辑器窗口

  1. 一些PC游戏只接收来自底层硬件层面的发送的键盘扫描码,而不响应例如keyevent()所用的pywinauto库发送的操作系统层面的虚拟键码,所以我们提供了新的接口,key_press(key)和key_release(key),来通过发送键盘扫描码进行键盘按键按下和释放的模拟。
  2. 一些游戏要求在按键按下和释放之间进行一定的操作,因此我们将keyevent(...)拆分成了按下和释放两个步骤,这样做也比传入一个duration参数要更加精确。
dev = device()
dev.key_press('W')  # 按下
# some operations
dev.key_release('W')  # 抬起
2.1.3.2、截屏 - snapshot()

源码

snapshot(filename=None, ensure_orientation=True, quality=10, max_size=None)
  • 截取一张当前手机画面,默认会发送到stdout。
  • 参数:
  1. filename – 保存截图的文件名,默认为None的话将会发送到stdout。
  2. ensure_orientation – 截图方向是否要与当前显示情况一致,默认为True。
  3. quality – 图像质量,整数范围为[1, 99]。
  4. max_size – 图片的最大尺寸,例如1200。
  • 返回:截图输出。
  • 注意与 poco.snapshot(width=720) 进行区分。

案例

def get_snapshot(img_name="", msg=None):
    """
    获取设备当面页面的截图,并且上传allure报告
    :param img_name: 图片的名称
    :param msg: 图片的备注,可不传
    """
    snapshot(
        # 截屏保存路径(可选)
        filename=f"./imgs/{img_name}.png",
        # 截屏备注(会显示在测试报告中)
        msg=msg,
        # 画质(0-100,默认10,值越高画质越好、文件越大)
        quality=90
    )

    # 上传allure报告
    allure.attach.file(
        source=f"./imgs/{img_name}.png",  # 本地截图路径
        name=img_name,
        attachment_type=allure.attachment_type.PNG
    )
2.1.3.3、cmd命令 - shell()

实际上,airtest的shell() ,就是帮忙补充了 adb -s 手机序列号 shell 这一部分,后面的内容就按正常shell指令来传就行了。

返回:shell指令的输出内容。

shell("pm list packages -3")

# 在当前设备上执行命令adb shell ls
print(shell("ls"))

# 对特定设备执行adb指令
dev = connect_device("Android:///device1")
dev.shell("ls")

# 切换到设备并执行adb命令
set_current(0)
shell("ls")
2.1.3.4、唤醒设备 - wake()
  1. 唤醒并解锁目标设备。
  2. 在部分品牌手机上可能无法生效。
wake()
dev.wake()
"""
支持平台:Android
"""
2.1.3.5、返回home界面 - home()

返回HOME界面。

home()
dev.home()
"""
支持平台:Android, iOS
"""
2.1.3.5、录屏操作
  1. Airtest所提供的录屏方法,目前该录屏方法已经兼容了Android、iOS、Windows上的脚本跑测录制。
  2. 下面这个两个方法,对于Android、iOS、Windows平台的参数可能不一样,使用时仔细看下对应的参数。下面以Android为例:
dev = device()
#开始录制
dev.start_recording(max_time, output, fps, mode, snapshot_sleep, orientation, bit_rate_level, bit_rate, max_size)

#结束录制
dev.stop_recording(output=None, is_interrupted=None)
参数说明
max_time=1800录屏最长时间限制,默认为1800,单位是秒。
output=None录屏保存文件地址, 默认为 None ,将会自动保存在log目录下,自动以当前时间命名。
fps=10输出视频的帧率,目前暂时限定不超过10fps,默认为10(仅对ffmpeg)。
mode="yosemite"保存视频的后台,可选 'ffmpeg'、'yosemite',模式的区别如下:
        (1)ffmpeg :对机型兼容较好,但cpu占用较高,第一次运行将会预先下载ffmpeg,有一定的额外空间占用(几十M)。
        (2)yosemite :Android旧版本的录屏方案,假如当前设备是Android,默认会使用这个模式录屏,必须要事先安装 Yosemite.apk ,并支持旧的参数,帧率比ffmpeg要高,但兼容性差,有一些手机不支持。
snapshot_sleep=0.001截图间隔。
orientation=0视频朝向模式 1 为竖屏;模式 2 为横屏;模式 0 为方形填充,默认是0。
bit_rate=None

bitrate 越高, 视频的清晰度更高。只支持Yosemite模式录屏。

max_size=None

能够指定录屏结果的图像大小,因为屏幕图片越大,在录屏时造成的系统负载越大(CPU和内存占用)。只支持ffmpeg模式录屏。

其中只有Android需要mode参数,默认为yosemite模式。
Window/iOS的start_recording不再需要有mode参数。
对于Android的录屏模式,我们可以按需去选择自己所需要的模式:

  • yosemite :原有的默认模式,录屏清晰度和质量较高,但兼容性较差,部分机型可能有问题。
  1. 支持的参数:max_time, orientation, bit_rate, bit_rate_level。
  • ffmpeg : 在Airtest 1.2.9版本之后加入的新模式,录屏帧率和质量较低,但兼容性更好,能兼容绝大多数设备。
  1. 支持的参数:max_time, orientation , max_size,fps, snapshot_sleep。

案例

from airtest.core.api import *
# 全局配置
ST.LOG_DIR = "./img/"  # 设置视频存放的位置

connect_device("Android:///")
dev = device()
dev.start_recording()  # 开启录屏

from poco.drivers.android.uiautomation import AndroidUiautomationPoco
poco = AndroidUiautomationPoco(use_airtest_input=True, screenshot_each_action=False)

start_app("park*************games")  # 启动app
sleep(6)
stop_app("park*************games")  # 退出app

dev.stop_recording()  # 结束录屏

2.1.4、应用权限授权

2.1.4.1、前言

在日常测试中,每次新安装应用或游戏都有一些前置的权限设置需要点击,但在不同的Android设备上的同意按钮都不完全相同,如果需要提高脚本的通用性以及复用性我们应该怎么办呢?那我们本周就一起来探讨一下这个问题。

2.1.4.2、pm授权与取消授权指令详解

PM工具,即包管理器(Package Manager),是Android开发和测试中不可或缺的工具。可以用于执行安装应用程序、查询应用包信息、管理系统权限以及控制应用程序。通常,PM工具位于系统的/system/bin目录下。

pm工具有两个指令参数,分别是 grant(授予应用权限许可)与 revoke(撤销应用权限),在Airtest环境的具体使用如下:

指令

#授予应用权限许可。必需android6.0(API级别23)以上的设备
shell(pm grant <packageName> <permission>)

#撤销应用权限。必需android6.0(API级别23)以上的设备
shell(pm revoke <packageName> <permission>)

指令参数

有些权限的设置有对应安卓版本的要求,如果版本没有达到要求,会报错。

permission(指令)指令说明
android.permission.POST_NOTIFICATIONS

应用的通知权限

Android 13(API 33)及以上

android.permission.READ_PHONE_STATE应用的电话权限
android.permission.READ_EXTERNAL_STORAGE应用存储的读取权限
android.permission.WRITE_EXTERNAL_STORAGE应用存储的写入权限
android.permission.ACCESS_COARSE_LOCATION应用打开位置权限
android.permission.CAMERA应用打开摄像头权限
android.permission.RECORD_AUDIO应用访问麦克风
android.permission.READ_CONTACTS应用读取联系人数据
android.permission.READ_SMS应用读取短信数据
android.permission.READ_CALENDAR应用读取设备的日历表
android.permission.WRITE_CALENDAR应用写入设备的日历表

案例

#允许应用存储的读写权限
shell("pm grant com.sina.weibo android.permission.READ_EXTERNAL_STORAGE")
shell("pm grant com.sina.weibo android.permission.WRITE_EXTERNAL_STORAGE")

#撤销应用存储的读写权限
shell("pm revoke com.sina.weibo android.permission.READ_EXTERNAL_STORAGE")
shell("pm revoke com.sina.weibo android.permission.WRITE_EXTERNAL_STORAGE")

总结

  1. 先设置允许所以的权限,再启动app。
  2. 我们在同意了APP权限之后,直接打开APP是可以跳过权限弹窗的,当我们撤销关闭掉权限之后,应用的权限请求弹窗是照常出现的。

2.1.5、其他

2.1.5.1、获取当前设备分辨率
width, height = device().get_current_resolution()

2.1.5.2、获取设备的序列号
device().get_default_device()

2.1.5.3、切换设备
switch_device(device)
2.1.5.4、打印出当前安装的app列表
device().list_app()
2.1.5.5、获取设备所有的第三方app
device().list_app(third_only=True)
2.1.5.6、获取应用的完整路径
device().path_app(package)

2.1.5.7、检查应用是否存在于当前设备上
device().check_app("com.netease.cloudmusic")

2.1.5.8、查看当前设备的显示信息
device().get_display_info()

orientation的值用于判断横竖屏。

2.2、图像识别核心 API

2.2.1、点击操作 - touch()

源码

touch(v, times=1, **kwargs)
  • 在当前设备画面上进行一次点击。
  • 参数:
  1. v – 点击位置,可以是一个Template图片实例,或是一个绝对坐标 (x, y)
  2. times – 点击次数。
  3. duration - 点击持续时间。
  4. kwargs – 平台相关的参数kwargs,请参考对应的平台接口文档。

案例

touch(Template(r"tpl1638179990578.png", record_pos=(0.179, -0.57), resolution=(810, 1440)))
  • Template:图像模板类,自动生成。
  1. 参数1:rgb:是否启用 RGB 三通道颜色匹配,默认False。
  2. 参数2:record_pos:截图时目标图像相对于屏幕中心的相对位置(坐标范围:-1 ~ 1)。
  3. 参数3:resolution:截取这张模板图片时的屏幕分辨率。
# 点击绝对坐标:
touch((100, 100))

# 点击图片的中心位置:
touch(Template(r"tpl1606730579419.png", target_pos=5))

# 点击相对坐标,例如,点击屏幕中心:
touch((0.5, 0.5))

# 点击两次:
touch((100, 100), times=2)

# 在Android和Windows下,可以设置点击持续时间:
touch((100, 100), duration=2)

# 右键点击(Windows):
touch((100, 100), right_click=True)

图像识别的全局阈值

上面介绍了图像模板类Template的各项参数,当我们修改那些参数时,只有对应的那张图片会生效,举个例子:
在这行代码中,我们将一张图片的识别阈值threshold设置为0.9,意思是当识别结果的可信度大于等于90%时,我们才认为这次图像识别匹配成功,是一个相当严格的设置了。

touch(Template(r"tpl1556019871196.png", threshold=0.9)

假如我们希望能够将这个设置扩展到整个脚本中的所有图片,又不希望挨个修改每张图片的代码时,我们可以考虑修改Airtest的全局配置来实现这个需求:

from airtest.core.api import *
# airtest.core.api中包含了一个名为ST的变量,即为全局设置
ST.THRESHOLD = 0.8

# 未指定图片threshold,默认使用ST.THRESHOLD中的0.8
touch(Template(r"tpl1532588127987.png", record_pos=(0.779, 0.382), resolution=(407, 264)))

# 手工指定图片threshold,以图片设置的0.6为准
touch(Template(r"tpl1532588127987.png", record_pos=(0.779, 0.382), resolution=(407, 264), threshold=0.6))

2.2.2、滑动操作 - swipe()

源码

swipe(v1, v2=None, vector=None, **kwargs)
  • 在当前设备画面上进行一次滑动操作。
  • 有两种传入参数的方式:
  1. swipe(v1, v2=Template(...)),从 v1 滑动到 v2。
  2. swipe(v1, vector=(x, y)),从 v1 开始滑动,沿着vector方向。
  • 参数:
  1. v1 – 滑动的起点,可以是一个Template图片实例,或是绝对坐标 (x, y)。
  2. v2 – 滑动的终点,可以是一个Template图片实例,或是绝对坐标 (x, y)。
  3. vector – 滑动动作的矢量坐标,可以是绝对坐标 (x,y) 或是屏幕百分比,例如 (0.5, 0.5)。
  4. **kwargs – 平台相关的参数 kwargs,请参考对应的平台接口文档。
  • 抛出:Exception – 当没有足够的参数来执行滑动时引发异常。
  • 返回:原点位置和目标位置。
  • 支持平台:Android, Windows, iOS。

案例

# 相对坐标:从屏幕左侧滑到右侧(适配所有设备)
swipe((0.1, 0.5), (0.9, 0.5), duration=0.5)  

# 从“左箭头”滑到“右箭头”
swipe(Template("left_arrow.png"), Template("right_arrow.png")) 

# 起点用图像,终点用相对坐标
swipe(Template("start_point.png"), (0.8, 0.5))  

# 使用滑动动作的矢量坐标
swipe(Template(r"tpl1606814865574.png"), vector=[-0.0316, -0.3311])

# 使用绝对坐标进行滑动
swipe((100, 100), (200, 200))

# 滑动持续1秒,分为6步
swipe((100, 100), (200, 200), duration=1, steps=6)

2.2.3、连续滑动 - swipe_along()

源码

swipe_along(coordinates_list, duration=0.8, steps=5)
  • 执行一段连续的滑动操作,仅适用于minitouch和maxtouch。
  • 参数:
  1. coordinates_list – 一个坐标的列表:[(x1, y1), (x2, y2), (x3, y3)]。
  2. duration – 滑动过程中的时间间隔,默认是0.8。
  3. steps – 滑动过程中的步数,默认为5。

案例

# 获取当前手机设备
dev = device()

# 手指按照顺序依次滑过多个坐标
dev.swipe_along([[959, 418],[1157, 564],[1044, 824],[751, 638],[945, 415]])

2.2.4、放大缩小图片 - pinch()

源码

pinch(in_or_out='in', center=None, percent=0.5)
  • 在设备屏幕上执行一个双指pinch捏合操作。
  • 参数:
  1. in_or_out - 向内捏合或向外扩大,在[“in",“out"]中枚举一个值。
  2. center - pinch动作的中心位置,默认值为None则为屏幕中心点。
  3. percent - pinch动作的屏幕百分比,默认值为0.5。
  • 支持平台:Android。

案例

# 获取当前手机设备
dev = device()

# 向内捏合
dev.pinch(in_or_out='in', center=None, percent=0.5)
sleep(1.0)

# 向外捏合
dev.pinch(in_or_out='out', center=None, percent=0.2)
sleep(1.0)

dev.pinch(in_or_out='out', center=None, percent=0.2)
sleep(1.0)

2.2.5、双指滑动 - two_finger_swipe()

源码

two_finger_swipe(tuple_from_xy,tuple_to_xy,duration=0.8,steps=5,offset=(0,50))
  • 执行双指滑动操作,仅适用于minitouch和maxtouch。
  • 参数:
  1. tuple_from_xy - 开始坐标(元组)。
  2. tuple_to_xy - 结束坐标(元组)。
  3. duration - 滑动持续时间的时间间隔,默认值为0.8。
  4. steps - 滑动步长,默认值为5。
  5. offset - 第二根手指的坐标偏移量,默认值为(0,50)。

案例

dev = device()  # 获取当前手机设备

 # 双指滑动   
dev.two_finger_swipe( (200, 900), (700, 900),duration=0.8, steps=5, offset=(0, 80))

dev.two_finger_swipe( (500, 1920), (500, 1300),duration=0.8, steps=5, offset=(0, 200))

2.2.6、拖拽操作 - drop_to()

  1. drop_to(start, end,duration)。
  2. 拖拽操作(从 start 到 end),duration表示持续时间。
# 从一个图片拖拽到另一个图片
drag(Template("item.png"), Template("bag.png"), duration=1)  

# 从一个图片拖拽到指定位置
drag(Template("item.png"), (0.5, 0.9), duration=1)  

2.2.7、输入文本 - text()

源码

text(text, enter=True, **kwargs)
  • 在目标设备上输入文本,文本框需要处于激活状态。
  • 参数:
  1. text – 要输入的文本。
  2. enter – 是否在输入完毕后,执行一次 Enter ,默认是True。
  3. search=True - 有些输入法需要点击搜索才能提交。
  • 支持平台:Android, Windows, iOS。

案例

# 输入文本test,并且按下回车
text("test")

# 输入文本test,并且没有按下回车
text("test", enter=False)

# 在Android上,有时你需要在输入完毕后点击搜索按钮
text("test", search=True)

2.2.8、等待元素出现 - wait()

源码

wait(v, timeout=None, interval=0.5, intervalfunc=None)
  • 等待当前画面上出现某个匹配的Template图片。
  • 参数:
  1. v – 要等待出现的目标Template实例。
  2. timeout – 等待匹配的最大超时时长,默认为None即默认取 ST.FIND_TIMEOUT的值。
  3. interval – 尝试查找匹配项的时间间隔(以秒为单位)。
  4. intervalfunc – 在首次尝试查找匹配失败后的回调函数。
  • 支持平台:Android, Windows, iOS。

案例

# 在ST.FIND_TIMEOUT后超时
wait(Template(r"tpl1606821804906.png")) 
# 每3秒查找一次模板,120秒后超时
wait(Template(r"tpl1606821804906.png"), timeout=120, interval=3)


# 你可以在每次查找目标失败时,指定一个回调函数
def notfound():
     print("No target found")

wait(Template(r"tpl1607510661400.png"), intervalfunc=notfound)

2.2.9、判断元素是否存在 - exists()

源码

exists(v)
  1. 检查设备上是否存在给定目标。
  2. 参数:v – 要检查的目标。
  3. 返回:如果未找到目标,则返回False,否则返回目标的坐标。
  4. 支持平台:Android, Windows, iOS。

案例

if exists(Template(r"tpl1606822430589.png")):
     touch(Template(r"tpl1606822430589.png"))

# 因为 exists() 会返回坐标,我们可以直接点击坐标来减少一次图像查找
pos = exists(Template(r"tpl1606822430589.png"))
if pos:
    touch(pos)

2.2.10、多图查找 - find_all()

源码

find_all(v)

在设备屏幕上查找所有出现的目标并返回其坐标列表。

参数:

  1. v:寻找目标。
  2. 返回:结果列表。
  3. 支持平台:Android, Windows, iOS。

案例

find_all(Template(r"tpl1607511235111.png"))
# [{'result': (218, 468), 'rectangle': ((149, 440), (149, 496), (288, 496), (288, 440)),'confidence': 0.9999996423721313}]

2.2.11、两图对比 - cal_ccoeff_confidence()

源码

cal_ccoeff_confidence(im_source, im_search)

案例

  1. cv2.resize 用于统一图片尺寸,上图中的(85,100)为我们指定的目标尺寸。  
  2. cv2.imread用于读取本地路径下的图片。
  3. cal_ccoeff_confidence用于对比两张图片的相似度,返回可信度的值。
# -*- encoding=utf8 -*-
__author__ = "AirtestProject"

from airtest.core.api import *
from airtest.aircv.cal_confidence import *

auto_setup(__file__)

img1 = cv2.resize(cv2.imread(r"frog.png"),(85,100))
img2 = cv2.resize(cv2.imread(r"chick.png"),(85,100))

confidence = cal_ccoeff_confidence(img1,img2)

print("----------------")
print(confidence)

2.3、断言

2.3.1、断言元素存在 - assert_exists()

源码

assert_exists(v, msg="")

断言目标存在,不存在则失败且打印后面的msg。

案例

assert_exists(Template("success_icon.png"), "登录成功图标未出现")

2.3.2、断言元素不存在 - assert_not_exists()

源码

assert_not_exists(v, msg="")

制作中...

2.3.3、断言值相等 - assert_equal()

源码

assert_equal(first, second, msg="", snapshot=True)
  1. 断言成功,不会打印后面的信息;断言失败,会打印后面的提示信息。
  2. eval()用于将字符串解析为可执行代码并返回运算结果,执行错误时抛出异常。
  3. snapshot参数,默认为snapshot=True,为了在设置断言时,还能附带截取当前画面的图片,然后显示在Airtest报告中。

案例

2.3.4、断言值不相等 - assert_not_equal()

源码

assert_not_equal(first, second, msg="", snapshot=True)
  1. 断言成功,不会打印后面的信息;断言失败,会打印后面的提示信息。
  2. snapshot参数,默认为snapshot=True,为了在设置断言时,还能附带截取当前画面的图片,然后显示在Airtest报告中。

案例

2.3.5、断言表达式为True/False

源码

assert_true(expr, msg="", snapshot=True)
assert_false(expr, msg="", snapshot=True)

案例

from airtest.core.assertions import *

# 断言表达式为True
assert_true(1==1, msg="assert 1==1")

# 断言表达式为False
assert_false(1==2, msg="assert 1!=2")

2.3.6、断言2个对象相同/不相同

源码

assert_is(first, second, msg="", snapshot=True)
assert_is_not(first, second, msg="", snapshot=True)

案例

from airtest.core.assertions import *

# 断言2个对象相同
assert_is(1, 1, msg="assert 1 is 1")

# 断言2个对象不相同
assert_is_not(1, 2, msg="assert 1 is not 2")

2.3.7、断言表达式为None/不为None

源码

assert_is_none(expr, msg="", snapshot=True)
assert_is_not_none(expr, msg="", snapshot=True)

案例

from airtest.core.assertions import *

# 断言表达式为None
assert_is_none(None, msg="assert None is None")

# 断言表达式不为None
assert_is_not_none(1, msg="assert 1 is not None")

2.3.8、断言第一个参数是否在第二个参数中

源码

assert_in(first, second, msg="", snapshot=True)
assert_not_in(first, second, msg="", snapshot=True)

案例

from airtest.core.assertions import *

# 断言第一个参数在第二个参数中
assert_in(1, [1, 2], msg="assert 1 in [1, 2]")

# 断言第一个参数不在第二个参数中
assert_not_in(3, [1, 2], msg="assert 3 not in [1, 2]")

2.3.9、断言对象是不是某种类型的实例

源码

assert_is_instance(obj, cls, msg="", snapshot=True)
assert_not_is_instance(obj, cls, msg="", snapshot=True)

这个断言语句中,第一个参数为obj,是一个具体的对象实例,第二个参数为cls,是一种类型,我们可以用这个断言来判断某个实例是不是属于某种类型的。

案例

from airtest.core.assertions import *

# 断言对象是某种类型的实例
assert_is_instance(1, int, msg="assert 1 is int")

# 断言对象不是某种类型的实例
assert_not_is_instance(1, str, msg="assert 1 is not str")

2.3.10、断言第一个值大于/大于等于第二个值

源码

assert_greater(first, second, msg="", snapshot=True)
assert_greater_equal(first, second, msg="", snapshot=True)

案例

from airtest.core.assertions import *

# 断言第一个值大于第二个值
assert_greater(2, 1, msg="assert 2 > 1")

# 断言第一个值大于等于第二个值
assert_greater_equal(1, 1, msg="assert 1 >= 1")

2.3.11、断言第一个值小于/小于等于第二个值

源码

assert_less(first, second, msg="", snapshot=True)
assert_less_equal(first, second, msg="", snapshot=True)

案例

from airtest.core.assertions import *

# 断言第一个值小于第二个值
assert_less(1, 2, msg="assert 1 < 2")

# 断言第一个值小于等于第二个值
assert_less_equal(1, 1, msg="assert 1 <= 1")

2.4、生成报告

2.4.1、生成html报告 - simple_report()

制作中...

2.5、图像处理模块 - arcv

2.5.1、查找图像 - loop_find()

源码

loop_find(query, timeout=ST.FIND_TIMEOUT, threshold=None, interval=0.5, intervalfunc=None)
  • 在屏幕中搜索图像模板,直到超时。
  • 参数:
  1. query:要在屏幕截图中查找的图像模板。
  2. timeout:查找图像模板的时间间隔。
  3. threshold:阈值,默认为None。
  4. interval:下次尝试查找图像模板前的休眠间隔。
  5. intervalfunc:在尝试查找图像模板失败后执行的函数。
  • 返回:如果未找到图像模板,则返回TargetNotFoundError,否则返回图像模板所在的位置。

案例

# 将我们的目标截图设置为一个Template对象
tempalte = Template("./yjcw_01.png")
# 在当前界面进行查找元素,并且返回其坐标
pos = loop_find(tempalte)
print(pos)  # (542, 1133)

2.5.2、保存截图到文件 - try_log_screen()

源码

try_log_screen(screen=None, quality=None, max_size=None)
  • 将屏幕截图保存到文件。
  • 参数:
  1. screen:待保存的屏幕截图,默认路径 ST.LOG_DIR。
  2. quality:图像质量,默认值为 ST.SNAPSHOT_QUALITY。
  3. max_size:图片的最大尺寸,例如1200。
  • 返回:返回一个字典,包含图片名称和大小。例如{'screen': '1766560922736.jpg', 'resolution': (261, 281)}。

2.5.3、局部找图 - match_in()

源码

Template(object).match_in(screen)
  • 在指定的图片对象中找图。
  • 参数:
  1. local_screen:待查找的局部图片,可以传入图片路径。
  2. screen:被查找的全局图片对象,不能传入图片路径。
  • 返回:找到就返回图片的位置,否则返回None。

案例

# 获取当前界面的截图
screen = G.DEVICE.snapshot()
# 局部截图,可以缩小查找的范围
# screen = aircv.crop_image(screen,(0,949,1067,1500))
# 将我们的目标截图设置为一个Template对象
tempalte = Template("./yjcw_01.png")
# 在截图里面查找指定的图片对象
pos1 = tempalte.match_in(screen)
print(pos)  # (542, 1133)

2.6、图像处理常用技巧

2.6.1、批量图像识别的快速遍历技巧

在日常UI测试过程中,如何快速准确地识别页面上的多个元素,或在日常测试中,如何高效地遍历目标图片列表,以确认画面中是否包含特定元素。

# -*- encoding=utf8 -*-
__author__ = "Airtest"

import os
from airtest.core.api import *
from airtest.aircv import *

auto_setup(__file__)

from poco.drivers.android.uiautomation import AndroidUiautomationPoco
poco = AndroidUiautomationPoco(use_airtest_input=True, screenshot_each_action=False)

def Multiple_exists(targets, area=None, threshold=0.80, rgb=False, inti=5):
    # 定义一个函数,用于检测多个目标是否存在
    # 参数说明:
    # targets: 目标列表
    # area: 搜索区域,默认为None(全屏)
    # threshold: 匹配阈值,默认为0.80
    # rgb: 是否使用RGB匹配,默认为False
    # inti: 尝试次数,默认为5次

    #新增找到的图片位置信息列表
    matches = []
    
    # 根据设备方向确定屏幕宽高
    if (G.DEVICE.display_info['orientation'] % 2):
        width = G.DEVICE.display_info['height']
        height = G.DEVICE.display_info['width']
    else:
        width = G.DEVICE.display_info['width']
        height = G.DEVICE.display_info['height']

    # 在指定的尝试次数内循环查找目标
    for i in range(inti):
        # 获取当前屏幕截图
        fullScreen = G.DEVICE.snapshot()
        # 遍历所有目标
        for target in targets:
            if target:
                # 构建目标图片的完整路径
                template_path = os.path.join(str(width), f"{target}.png")
                print(f"Trying to load template: {template_path}")
                # 在指定区域内查找目标
                focus_pos = match_in_predict_area(Template(template_path, threshold=threshold, rgb=rgb), fullScreen, area)
            #若找到图片则将当前图片出现的位置传入列表中
                if focus_pos:
                    matches.append((targets.index(target), focus_pos))
        # 如果所有目标都找到,立即返回结果
        if len(matches) == len(targets):
            return matches
               
        # 如果未找到所有目标,等待0.2秒后继续下一次尝试
        sleep(0.2)
    # 返回找到的所有匹配结果
    return matches       

def match_in_predict_area(template, screen=None, rect=None):  # 局部找图
    if screen is None:
        if G.DEVICE is None:
            raise Exception("G.DEVICE is none.")
    screen = G.DEVICE.snapshot()  # 获取设备屏幕截图
    if screen is None:
        raise Exception("snapshot is none.")
    if rect is None:
        return template.match_in(screen)  # 如果没有指定区域,在整个屏幕中查找
    if not isinstance(rect, (list, tuple)):
        raise Exception("to crop a image, rect should be a list")
    else:
        # logger.debug("找到目标%s"%template.filename)
        predict_screen = aircv.crop_image(screen, rect)  # 裁剪指定区域的图片
        focus_pos = template.match_in(predict_screen)  # 在裁剪后的图片中查找模板
        if not focus_pos:
            return False  # 如果没有找到匹配,返回False
        else:
            return focus_pos[0]+rect[0], focus_pos[1]+rect[1]  # 返回匹配位置,加上裁剪区域的偏移

if __name__ == "__main__":   
 #打开游戏,去确认是否游戏开始界面的元素已就位
    start_app("com.netease.dyll")
    sleep(1.0)
    poco(text="确定").click()
    touch([0.5,0.5])
    sleep(0.2)

    # 定义目标图片的路径列表
    targets = [r"D:/demo/pics.air/tpl1724135823600",r"D:/demo/pics.air/tpl1724135830426",r"D:/demo/pics.air/tpl1724135836266"]
    
    # 调用multiple_exists函数,查找游戏开始界面的图片识别情况
    result = Multiple_exists(targets)

#     打印查找结果
    for i in range(len(result)):
        print("图片{}的位置是:{}".format(i+1,result[i]))
        
    # 判断是否已进入游戏开始界面
    if len(result) == len(targets) :
        print("确认已进入到游戏的开始界面")
  1. 代码逻辑的核心在于快速地识别目标图像。
  2. 首先,将目标图像(最好是特征鲜明、尺寸小一些)列表输入Multiple_exists()函数。该函数通过循环执行截图操作,每0.2秒进行一次,以最小化循环识别时间。
  3. 接着将设备屏幕截图和目标图像传递给match_in_predict_area()函数,进行裁剪和搜索。
  4. 一旦找到匹配的图像,立即将坐标信息反馈给Multiple_exists()函数,并最终将图像编号和位置信息返回至主函数,供进一步使用。

2.6.2、Airtest结合Poco对控件实施精准截图

  • 要实现“通过选择器获取到了控件,然后截图这个控件范围”,根据我们的Airtest的局部截图接口以及poco控件的属性查询接口是可以很快实现的。
  • 可以用于在手机桌面上截取app的icon。
  • 实现:
  1. 截取设备的整个屏幕:G.DEVICE.snapshot()
  2. 将控件的局部坐标转换成全局坐标。
  3. 获取局部截图:crop_image(img, rect)。
from airtest.core.api import *
from airtest.aircv import *

# 全局配置
ST.LOG_DIR = "./img/"  # 设置图片存放的位置
ST.SNAPSHOT_QUALITY = 90  # 设置截图的质量

connect_device("Android:///")

from poco.drivers.android.uiautomation import AndroidUiautomationPoco
poco = AndroidUiautomationPoco(use_airtest_input=True, screenshot_each_action=False)

dev = device()
# 获取屏幕的分辨率,返回元组()
wh = dev.get_current_resolution()
# 截图
screen = G.DEVICE.snapshot()
# 定位控件
if poco("Car Out").exists():
    # 获取控件的大小,返回列表[]
    size = poco("Car Out").get_size()
    print(size)
    # 获取控件的位置,返回列表[]
    pos = poco("Car Out").get_position()
    print(pos)
    # 计算左上角坐标,转化成绝对坐标
    x1 = int((pos[0] - size[0] * 0.5) * wh[0])
    y1 = int((pos[1] - size[1] * 0.5) * wh[1])
    # 计算右下角坐标,转化成绝对坐标
    x2 = int((pos[0] + size[0] * 0.5) * wh[0])
    y2 = int((pos[1] + size[1] * 0.5) * wh[1])
    # 局部截图
    screen = aircv.crop_image(screen, (x1, y1, x2, y2))
    # 保存局部截图到img文件夹中
    try_log_screen(screen)

2.6.3、点击指定文字的坐标

安装第三方库

pip install rapidocr-onnxruntime

基础使用

from rapidocr_onnxruntime import RapidOCR


def find_text_coords(image_path, target_text, contains=True):
    ocr = RapidOCR()
    result, elapsed = ocr(image_path)

    matches = []
    for box, text, score in result:
        ok = (target_text in text) if contains else (target_text == text)
        if not ok:
            continue

        xs = [p[0] for p in box]
        ys = [p[1] for p in box]
        left, top, right, bottom = min(xs), min(ys), max(xs), max(ys)
        cx, cy = (left + right) / 2, (top + bottom) / 2

        matches.append({
            "text": text,
            "score": float(score),
            "box4": box,
            "rect": (left, top, right, bottom),
            "center": (cx, cy),
        })
    return matches


print(find_text_coords("mmzy.png", "确定"))
"""
[
    {
        'text': '确定', 
        'score': 0.996717780828476, 
        'box4': [[476.6696472167969, 1926.9254150390625], 
                [608.5402221679688, 1926.9254150390625], 
                [608.5402221679688, 1998.6531982421875], 
                [476.6696472167969, 1998.6531982421875]], 
        'rect': (476.6696472167969, 1926.9254150390625, 608.5402221679688, 1998.6531982421875), 
        'center': (542.6049346923828, 1962.789306640625)
    }
]
"""

封装到Airtest中使用

    def click_text(target_text, contains=True):
        """
        输入文本,进行点击操作
        :param target_text: 目标文本
        :param contains: True为包含, False为完全一致
        """
        # 获取设备的截图
        screen = G.DEVICE.snapshot()
        # 创建 OCR 对象并识别图片
        ocr = RapidOCR()
        # result:识别结果列表, elapsed:耗时(秒或毫秒,具体看库实现, 一般就是用来调试性能)
        result, elapsed = ocr(screen)

        # 准备存放匹配结果
        matches = []
        # 遍历每一条 OCR 识别结果
        for box, text, score in result:
            ok = (target_text in text) if contains else (target_text == text)
            # 判断该条是否匹配目标文字
            if not ok:
                continue
            # 从四点框提取 x、y,并计算外接矩形
            xs = [p[0] for p in box]
            ys = [p[1] for p in box]
            left, top, right, bottom = min(xs), min(ys), max(xs), max(ys)
            # 这里实际是浮点数的坐标,我强装成整数
            cx, cy = int((left + right) / 2), int((top + bottom) / 2)

            matches.append({
                "text": text,  # 实际识别到的文字
                "score": float(score),  # 置信度
                "box4": box,  # 原始四点框(更精确,适合做图像标注/判断倾斜)
                "rect": (left, top, right, bottom),  # 外接矩形 (left, top, right, bottom)
                "center": (cx, cy),  # 中心点
            })
        # return matches
        # 执行点击操作
        if matches is not None:
            touch(matches[0]['center'])

2.7、全局配置

2.7.1、G类(单例实例)

  1. G 是Airtest内置的全局上下文对象(来自 airtest.core.api import G),本质是一个封装了全局变量的命名空间,用于存储整个测试会话中“跨模块/跨函数共享”的核心资源。
  2. 简单说:G 是Airtest用来存放全局关键资源的容器,避免开发者手动传递这些核心对象。
class G(object, metaclass=DeviceMetaProperty):
    """Represent the globals variables"""
    BASEDIR = []
    LOGGER = AirtestLogger(None)
    LOGGING = get_logger("airtest.core.api")
    SCREEN = None
    _DEVICE = None
    DEVICE_LIST = []
    RECENT_CAPTURE = None
    RECENT_CAPTURE_PATH = None
    CUSTOM_DEVICES = {}
属性名类型作用说明
BASEDIRlist存储脚本的基础目录(可多目录),用于查找截图模板、配置文件等。
LOGGERAirtestLoggerAirtest 封装的日志器(核心),负责记录步骤、生成报告、输出彩色日志。
LOGGINGlogging.LoggerPython 标准库日志器,用于 Airtest 内部底层日志输出(区别于业务日志)。
SCREENScreen/None

屏幕捕捉器实例(不同设备有不同实现:AndroidScreen/WindowsScreen)。

snapshot() 底层调用 G.SCREEN.snapshot() 实现截屏。

_DEVICE(私有)Device/None

存储当前活跃的设备实例(Android/iOS/Windows)。

元类映射到 G.DEVICE,开发者无需直接访问。

DEVICE(元类映射)Device/None对外暴露的 “当前设备实例”,所有设备操作的核心入口
DEVICE_LISTlist存储所有已连接的设备实例(多设备场景)。
RECENT_CAPTUREPIL.Image/None存储最近一次截屏的图像数据(PIL 对象),用于断言、二次处理。
RECENT_CAPTURE_PATHstr/None存储最近一次截屏的文件路径,生成报告时自动关联该路径。
CUSTOM_DEVICESdict注册自定义设备类型(扩展 Airtest 支持的设备)。

2.7.2、G.DEVICE

  • G.DEVICE 赋值时机:
  1. 只有通过 auto_setup()、connect_device() 成功连接设备后,G.DEVICE 才会被赋值。
  2. 未连接设备时调用 G.DEVICE 会抛出 AttributeError(提示 'NoneType' object has no attribute 'xxx')。
  • 多设备场景的坑:
  1. 若未用 set_current() 切换设备,G.DEVICE 始终指向 “第一个连接的设备”。
  2. 多设备操作时,务必先 set_current(dev) 再调用 G.DEVICE,避免操作错设备。
  • 与 Poco 的关系:
  1. Poco 初始化时会自动绑定 G.DEVICE(当前设备),因此 Poco 的控件操作(如 poco("btn").click())最终也会通过 G.DEVICE 执行底层点击。

G.DEVICE.display_info[ ]

display_info[ ]:设备的显示信息字典,由Airtest从设备底层(如Android的ADB命令、Windows的系统 API)获取,包含屏幕分辨率、旋转方向、像素密度等关键参数。

orientation的值用于判断横竖屏

  1. display_info['orientation']
  2. display_info字典中键为orientation的值,代表屏幕的旋转方向/角度,常见取值为:
返回值安卓/iOSWindows
0正竖屏竖屏
90正横屏横屏
180倒竖屏-
270倒横屏-
# 根据设备方向确定宽度和高度
    if (G.DEVICE.display_info['orientation'] % 2):
        # 横屏
        width = G.DEVICE.display_info['height']
        height = G.DEVICE.display_info['width']
    else:
        # 竖屏
        width = G.DEVICE.display_info['width']
        height = G.DEVICE.display_info['height']

操作部分的源码

@logwrap
def wake():
    """
    Wake up and unlock the target device

    :return: None
    :platforms: Android
    :Example:
        >>> wake()

    .. note:: Might not work on some models
    """
    G.DEVICE.wake()


@logwrap
def home():
    """
    Return to the home screen of the target device.

    :return: None
    :platforms: Android, iOS
    :Example:
        >>> home()
    """
    G.DEVICE.home()

说明:大部分方法底层都是直接调用上面进行执行

  1. touch() 底层调用 G.DEVICE.touch(pos, **kwargs)。
  2. snapshot 底层调用 G.DEVICE.snapshot(filename, quality=quality, max_size=max_size)。
  3. ........

总结

G.DEVICE 是 Airtest 对 “当前活跃设备” 的全局底层引用:

  1. 对普通用户:日常用快捷 API(touch()、snapshot())即可,无需直接操作 G.DEVICE。
  2. 对进阶用户:当需要获取设备底层信息、调用未封装的设备功能、多设备精准控制时,G.DEVICE 是核心入口。

2.7.3、ST类

Settings是Airtest内置的全局配置类,源码中会被实例化为单例 ST(from airtest.core.settings import ST),作用是:

  1. 统一存储自动化运行的所有核心参数(无需零散定义全局变量);
  2. 提供合理的默认值,兼顾通用性和稳定性;
  3. 支持动态修改(全局/局部),适配不同测试场景(如调整图像匹配精度、超时时间);
  4. 兼容环境变量和版本差异(如 OpenCV 版本、项目路径)。

源码

class Settings(object):

    DEBUG = False
    LOG_DIR = None
    LOG_FILE = "log.txt"
    RESIZE_METHOD = staticmethod(cocos_min_strategy)
    # keypoint matching: kaze/brisk/akaze/orb, contrib: sift/surf/brief
    CVSTRATEGY = ["mstpl", "tpl", "sift", "brisk"]
    if Version('3.4.2') < Version(cv2.__version__) < Version('4.4.0'):
        CVSTRATEGY = ["mstpl", "tpl", "brisk"]
    KEYPOINT_MATCHING_PREDICTION = True
    THRESHOLD = 0.7  # [0, 1]
    THRESHOLD_STRICT = None  # dedicated parameter for assert_exists
    OPDELAY = 0.1
    FIND_TIMEOUT = 20
    FIND_TIMEOUT_TMP = 3
    PROJECT_ROOT = os.environ.get("PROJECT_ROOT", "")  # for ``using`` other script
    SNAPSHOT_QUALITY = 10  # 1-100 https://pillow.readthedocs.io/en/5.1.x/handbook/image-file-formats.html#jpeg
    # Image compression size, e.g. 1200, means that the size of the screenshot does not exceed 1200*1200
    IMAGE_MAXSIZE = os.environ.get("IMAGE_MAXSIZE", None)
    SAVE_IMAGE = True
2.7.3.1、调试与日志配置
属性默认值作用
DEBUGfalse调试模式开关:开启后输出更详细的底层日志(如图像匹配的关键点、耗时)
LOG_DIRNone日志存储目录:默认随脚本目录,设置后日志 / 截屏会存入该目录
LOG_FILE"log.txt"日志文件名:核心日志(步骤、错误)会写入该文
2.7.3.2、图像识别核心配置
属性RESIZE_METHOD
初始值\逻辑绑定 cocos_min_strategy 静态方法。
作用图像缩放策略:解决不同分辨率设备的匹配问题(cocos_min_strategy 是为 Cocos 游戏优化的缩放算法,也可替换为 airtest.core.error_reload.resize_keep_ratio 等)。
属性CVSTRATEGY
初始值\逻辑默认为 ["mstpl", "tpl", "sift", "brisk"]。OpenCV 3.4.2~4.4.0 时剔除 sift。
作用图像匹配算法优先级列表(按顺序尝试):
- mstpl:多尺度模板匹配(基础)
- tpl:普通模板匹配
- sift/surf:关键点匹配(需 OpenCV Contrib)
- brisk:轻量级关键点匹配(兼容性好)
属性KEYPOINT_MATCHING_PREDICTION
初始值\逻辑True
作用关键点匹配时是否启用「位置预测」:加速匹配(基于历史位置预判目标区域)。
属性THRESHOLD
初始值\逻辑0.7(0~1)
作用通用匹配阈值:匹配相似度≥该值则认为成功(值越高越严格,避免误匹配;值越低越宽松,避免漏匹配)。
属性THRESHOLD_STRICT
初始值\逻辑None
作用assert_exists 断言专用阈值:未设置时复用 THRESHOLD,设置后优先使用。
2.7.3.3、超时与操作延迟配置
属性初始值作用
OPDELAY0.1

每次操作(touch/swipe/text)后的延迟时间(秒):

避免操作过快导致设备无响应。

FIND_TIMEOUT20

全局查找元素(图像 / 控件)的超时时间(秒):

wait()/exists() 等默认使用。

FIND_TIMEOUT_TMP3临时查找超时(如模板匹配的快速校验):用于非核心步骤的快速判断。
2.7.3.4、路径与环境配置
属性初始值作用
PROJECT_ROOT读取环境变量PROJECT_ROOT,默认空。项目根路径:使用 using() 导入其他脚本时,会从该路径查找脚本 / 模板。
2.7.3.5、截屏相关配置
属性初始值作用
SNAPSHOT_QUALITY10(1-100)截屏压缩质量:数值越大画质越好(100为无损),文件体积也越大。
IMAGE_MAXSIZE读取环境变量IMAGE_MAXSIZE,默认None截屏最大尺寸限制:如设为1200,则截屏宽/高不超过1200px(等比缩放)。
SAVE_IMAGETrue是否保存截屏文件:关闭后仅内存缓存,不写入磁盘。

三、Poco UI定位与操作

Poco是一款基于Python语法的、与引擎无关的自动化测试框架,它基于控件搜索原理,可以准确定位到应用里面的控件,并且对控件进行点击、滑动等操作。

class Poco(agent, **options)
  • Agent(PocoAgent)– Poco用于与目标设备通信的代理对象。
  • options
  1. action_interval:等待在设备上执行的操作(如触摸或滑动)完成以及UI变得静止(稳定)所需的时间间隔。默认值为0.8秒。
  2. poll_interval:每次轮询事件(例如等待 UI 元素出现在屏幕上)之间所需的最短时间。轮询是定期进行的。
  3. pre_action_wait_for_appearance:执行操作(如点击或滑动)前等待的时间间隔。如果在此时间间隔后目标 UI 元素未出现在屏幕上,则会抛出 PocoNoSuchNodeException 异常。
  4. touch_down_duration:点击操作按下步骤的持续时间。如果提供此参数,该值将被设置到 self.agent.input 模块中。请注意,并非所有 poco 的实现都支持此参数。如果不支持,你可能会看到一条警告信息。
  5. reevaluate_volatile_attributes: 在检索易变属性时重新选择目标 UI 代理。使用 hrpc 连接的 Poco 驱动程序默认应设为 False,因为 hrpc 总是在远程重新评估属性。此选项对 StdPoco 驱动程序很有用,应由 StdPoco 处理。

3.1、poco初始化

3.1.1、Unity初始化

在对Android/iOS设备上的unity游戏应用进行Poco初始化之前,请务必确保它已经事先接入了Poco-SDK。如果我们已经在游戏里正确接入了Poco-SDK,那么游戏启动后,Poco服务也会启动,所以我们要确保在游戏完全启动之后,再来进行unity Poco的初始化。

from poco.drivers.unity3d import UnityPoco
poco = UnityPoco()

3.1.2、UE4初始化

from poco.drivers.ue4 import UE4Poco
poco = UE4Poco()

3.1.3、Cocos-lua初始化

from poco.drivers.std import StdPoco
poco = StdPoco()

3.1.4、Cocos-js初始化

from poco.drivers.cocosjs import CocosJsPoco
poco = CocosJsPoco()

3.1.5、Android原生初始化

from poco.drivers.android.uiautomation import AndroidUiautomationPoco
poco = AndroidUiautomationPoco(use_airtest_input=True, screenshot_each_action=False)
  1. use_airtest_input=True是指,使用Airtest去执行点击操作,好处是,会在日志里面记录一条log记录,这样生成报告时,就能在报告上显示这个点击记录。
  2. screenshot_each_action=False是指,是不开启每个poco步骤的截图。
  3. 至于,为什么当初始化时,指定use_airtest_input=True,且screenshot_each_action=False时,仍然会有截图,这是因为使用了Airtest的点击(click),Airtest点击是默认开启截图的。

3.1.6、iOS原生初始化

from poco.drivers.ios import iosPoco
poco = iosPoco()

3.1.7、Std-broker初始化

from poco.drivers.std import StdPoco
from poco.utils.device import VirtualDevice
poco = StdPoco(15004, VirtualDevice('localhost'))

3.2、三种元素定位选择器

Poco控件最基本的3种定位选择器分别是:

  1. 基本选择器
  2. 相对选择器
  3. 空间顺序选择器

3.2.1、基本选择器(直接/正则)

  1. 在poco实例后加一对括号,括号里的参数就是所给定的条件,比如节点的属性及预期的属性值。选择器会遍历所有元素,将满足给定条件的元素都选出来并返回。
  2. 常用的属性有name、text、type、tag等,属性默认值为name。

定位控件

  1. 在poco实例后加一对括号,我们就可以进行元素选择了。选择器会遍历所有元素,将满足给定条件的元素都选出来并返回。
  2. 括号里的参数就是所给定的条件,用属性名值对表示,其中第一个参数表示 节点名 ,就像 poco("star_single") 。
  3. 后面还可以跟着一些可选参数,均表示节点的属性及预期的属性值。
poco("star_single",type="Image")

利用正则表达式匹配控件

使用方法:poco(xxMatches="预期属性值的正则表达式")

  1. textMatches(),使用text的正则表达式进行匹配。
  2. nameMatches(),使用name的正则表达式进行匹配。
  3. typeMatches(),使用type的正则表达式进行匹配。
poco(nameMatches="com.*?songInfo")
poco(textMatches='^close.*$')

检测正则表达式是否匹配的网站 :https://tool.oschina.net/regex/ 。


3.2.2、相对选择器(关系)

如果直接用节点属性(或者说仅仅使用基本选择器)没法选出你所想要的元素时,你还可以通过元素之间的渲染层级关系进行选择,例如父子关系、兄弟关系、祖先后代关系等等。

方法描述
parent()查找某个元素的直接父节点,括号内不可传入参数。
child()查找某个元素的直接子节点,括号内可给定节点的属性值。
children()查找某个元素的直接子节点,括号内不可传入参数。
offspring()查找某个元素的后代元素,包含子节点,孙节点等,括号内可给定节点的属性值。
sibling()查找某个元素的兄弟节点,括号内可给定节点的属性值。
# 查找Text(1)的父类节点
tp = poco("Text(1)").parent()
# 查找Text(1)的兄弟节点Text(12)
ts = poco("Text(1)").sibling("Text(12)")
# 查找的Content子节点Text(1)
tc = poco("Content").child("Text(1)")
# 查找Scroll View的孙子节点Text(1)
to = poco("Scroll View").offspring("Text(1)")

print(f"Text(1)的父类:{tp}")
print(f"Text(1)的兄弟:{ts}")
print(f"Content的孩子:{tc}")
print(f"Scroll View的孙子:{to}")

3.2.3、空间顺序选择器(位置)

  1. 若寻找的元素存在大量的重名,没有办法准确定位时,就采用空间顺序选择器。
  2. 先定位到元素控件名称相同的一批元素,然后通过下标去定位要访问的元素。
from poco.drivers.unity3d import UnityPoco
poco = UnityPoco()

chs = poco("Content").child(type="Text")

print(f"子元素的数量:{len(chs)}")
print(chs[0].get_name())
print(chs[1].get_name())
print(chs[2].get_name())

3.3、元素操作

poco只能接受绝对坐标(归一化坐标系)。

例图

3.3.1、点击元素 - click()

  1. 先使用poco定位到元素,再进行点击操作。
  2. 定位可以通过元素的:name(默认)、type。
# 点击元素的中心位置
poco("blue").click()

click里面可以传入参数,来调整点击的位置

# 点击元素的中心位置
poco("blue").click([0.5,0.5])
# 点击元素的左上角位置
poco("blue").click([0,0])
# 点击元素的右上角位置
poco("blue").click([1,0])
# 点击元素的左下角位置
poco("blue").click([0,1])
# 点击元素的右下角位置
poco("blue").click([1,1])

3.3.2、长按元素 - long_click()

  1. 先使用poco定位到元素,再进行点击操作。
  2. 定位可以通过元素的:name(默认)、type。
  3. 默认长按2s,可以传入参数,设定时间。
# 默认长按2s
poco("blue").long_click()
# 可以传入参数,设定时间
poco("blue").long_click(10)

3.3.3、双击元素 - double_click()

  1. 先使用poco定位到元素,再进行点击操作。
  2. 定位可以通过元素的:name(默认)、type。
# 双击
poco("blue").double_click()

3.3.4、指定基准点 - focus()

  1. 可以通过focus来定位需要操作的位置。
  2. [0,0]表示左上角,[0.5,0.5]表示中心点,[1,1]表示右下角。
poco("blue").focus([0.5,0.5]).click()

cilck()与focus()使用上的区别:

  1. click()的操作,只能对当前的语句生效,下面其他的就还是默认值。
  2. focus()如果一旦设定了,后面使用当前控件去操作,会保留focus的设定。
blue_btn = poco("blue").focus([1,1]) # 点击右下角
blue_btn.click()

3.3.5、滑动元素 - swipe()

定位元素进行滑动

  1. 先定位一个元素,然后选择方向移动。
  2. 参数 duration 来控制滑动时间。
# 先定位元素,然后选择方向移动
poco("blue").swipe("up",duration=2) # 向上
poco("blue").swipe("down",duration=2) # 向下
poco("blue").swipe("left",duration=2) # 向左
poco("blue").swipe("right",duration=2) # 向右

按照屏幕比例进行滑动

  1. poco.swip([x1,y1],[x2,y2],duration),按照比例滑动,归一化坐标。
  2. 参数 duration 来控制滑动时间。
# 按照比例进行滑动,从屏幕中间,从右往左滑动
poco.swipe([0.7,0.5],[0.2,0.5],duration=2)

poco基于UI对象的swipe

以定位的元素的左上角为原点,进行滑动。

# 向下滑动0.2个单位距离
poco("Sprite_1ze").swipe([0,0.2])
# 向上滑动0.2个单位距离
poco("Sprite_1ze").swipe([0,-0.2])
# 向左滑动0.2个单位距离
poco("Sprite_1ze").swipe([-0.2,0])
# 向右滑动0.2个单位距离
poco("Sprite_1ze").swipe([0.2,0])

3.3.6、拖拽元素 - drop_to()

  1. 从一个元素拖拽到另一个元素上面。
  2. 参数 duration 来控制拖拽时间。
poco("blue").drag_to(poco("black"),duration=2)

3.3.7、输入文本 - set_text()

先定位到可输入文本的元素,然后输入内容。

poco("pos_input").set_text("hello")

3.4、属性获取

3.4.1、获取元素属性 - attr()

通过attr()函数,可以获取元素本身的一些属性值。

# 获取属性的name
poco("blue").attr("name")
# 获取属性的type
poco("blue").attr("type")
# 获取属性的text
poco("blue").attr("text")
# 获取属性的tap
poco("blue").attr("tag")

3.4.2、判断元素存在 - exists()

判断当前界面,所定位的元素是否存在,存在则返回True,否则返回False。

poco("blue").exists()

3.4.1、元素树冻结 - freeze()

  1. UI自动化的用例中,有时候需要频繁的操作UI,因为poco的所有操作,几乎每次操作前,都要通过发送rpc请求把App的UI树从手机拉到PC,如属性类操作attr()、各种等待、各种点击等,这将非常耗费时间。这么做的原因是Poco无法保证App界面有没有发生变化,所以每次操作前都要去获取实时的UI树。
  2. 所以Poco提供了freeze()方法,它把UI树拉取一次后,保存在PC内存中,以后再有交互操作时,不再去手机拉取,而是直接读取PC内存中保存的这份UI树。这将极大提高操作效率从而节省UI自动化执行时间。
  3. 不过这种方法有一个缺点,就是在你这段操作时间内,UI不能有变化。比如当你点击第一个复选框后,在该复选框下面又多出来一行输入框,那么后面的复选框都要往下移。如果此时仍用保存在PC内存中的hierarchy(UI树),在你去点击第2个复选框时,实际是点到了新出现的输入框上面。

冻结UI,将当前UI树复制一份,不保证与手机实时屏幕一致,但操作非常快速。

with poco.freeze() as freeze_poco:
    items= freeze_poco("Shudu2GameLayer").child(type="Sprite").child(type="LabelAtlas")
    for item in items:
        item.click()

3.4.2、强制等待 - wait()

  1. 仅等待不报错
  2. 先定位到某个元素,等待设定的时间后,再进行其他的操作。

注意:Poco控件的wait方法,即使在设定时间内未找到控件,也是不会报错的,可以继续往下执行下去。

另外我们需要区分下Airtest的wait方法,等待图片目标出现wait(图片);和Poco控件的wait方法,等待控件出现poco(xxx).wait(timeout=3)。

# 等待3s后,再输入文本
poco("pos_input").wait(3).set_text("hello")
# 等待3s后,再判断元素是否存在
poco("pos_input").wait(3).exists()

3.4.3、等待元素出现 - wait_for_appearance()

3.4.4、等待元素消失 - wait_for_disappearance()

  1. 等待,不满足则报错。
  2. 轮询查找,找到/找不到就结束轮询。
  3. Poco控件还支持另外2个等待事件,wait_for_appearance()和wait_for_disappearance()。这两个API可以帮助我们等待页面上某1个UI出现或者消失,等待的超时时间timeout默认为120秒。
  4. 如果在超时时长之内元素没有出现或者消失的话,会报PocoTargetTimeout的错误。
# 等待黄色出现
poco("yellow").wait_for_appearance(timeout=20)

# 等待计分文本控件消失
poco(text="Count:").wait_for_disappearance(timeout=3)

3.4.5、等待任意一个元素出现 - wait_for_any()

wait_for_any() 则是在超时时长结束之前,等待任意一个UI显示出来,即一次轮询任何一个给定的UI。

bomb = poco("bomb")
yellow = poco("yellow")
blue = poco("blue")

while True:
    fish = poco.wait_for_any([bomb,yellow,blue])
    print(fish.get_name())

3.4.6、等待所有的元素出现 - wait_for_all()

wait_for_all() 是在超时时长结束之前,需要 等待所有给定的UI对象都显示出来 ,即一次轮询所有UI,例如等待三个图标都显示之后,再点击返回按钮。

yellow = poco("yellow")
blue = poco("blue")
black = poco("black")

poco.wait_for_all([yellow,blue,black])
poco("btn_back").click()

3.5、遍历Poco元素

制作中...

3.6、Poco测试小程序

简介和设置

微信和QQ内置的浏览器内核不是Android原生的WebView,而是他们自家的X5 WebView内核,我们需要切换到系统WebView内核,这样Poco才能更好地识别出小程序的各种UI控件。
设置:

  1. 打开微信中任意一个聊天框,输入 debugtbs.qq.com,点击进入TBS调试页面,再点击页面内的“安装线上内核”;
  2. 安装成功后,我们需要重启微信才可以生效;
  3. 再次进去刚才的TBS调试页面,检查我们的设置是否生效,点击“查看版本信息”,版本信息为 0(null) 则设置成功;
  4. 设置成功以后即可用Poco对小程序进行测试。

Poco测试小程序实操

注意

  1. TBS调试只适用于Android系统,iOS暂不支持。
  2. 在小程序测试的实操中,我们可以灵活地交替使用Airtest和Poco脚本,比如:有些编辑框无法使用Poco的set_text接口,我们可以先使用poco进行点击操作,再用Airtest的text接口。

3.7、Poco常用技巧

3.7.1、提高遍历pocoUI树的效率

在Poco的实际应用过程中,我们可能会经常遇到需要遍历pocoUI树的情况,整个项目的UI树,体积有可能非常庞大。

  1. 方法一:offspring(),每次对poco实例的属性进行操作,都需要重新生成一个dump,这非常耗费资源。 除非在之后的业务逻辑里要对offspring()进行不可同步的操作,否则不建议采取这种方法。
  2. 方法二:offspring("参数"),offspring()方法里跟指定参数,可以将dump操作缩减为1次,大大提升效率。
  3. 方法三:freeze(),一次抓取(消耗几百毫秒),可以使用多次,freeze()的效率极高,但需要手动刷新。

3.7.2、swipe的优化

  1. 在使用过程中,我们会看见有两个swipe,一个是Airtest中的swpie,另一个是poco.swpie。
  2. 推荐使用:Airtest中的swipe+获取设备的分辨率*百分比。
  3. 不太推荐使用poco.swpie的原因是底层处理较复杂,耗时较长。

Airtest中的swpie

from airtest.core.api import *
from airtest.core.android.touch_methods.base_touch import *

connect_device("Android:///")

stat_time = time.time()
# 获取设备的高度和宽度
width, height = device().get_current_resolution()
star = (width * 0.9, height * 0.5)
end = (width * 0.1, height * 0.5)
# 滑动屏幕
swipe(star, end)

end_time = time.time()
print(end_time - stat_time)  # 2.5876848697662354

poco.swpie

from airtest.core.api import *
from airtest.core.android.touch_methods.base_touch import *

connect_device("Android:///")

from poco.drivers.android.uiautomation import AndroidUiautomationPoco
poco = AndroidUiautomationPoco()

stat_time = time.time()
# 滑动屏幕
poco.swipe([0.9, 0.5], [0.1, 0.5])

end_time = time.time()
print(end_time - stat_time)  # 3.09871244430542

四、自定义事件

  • baset_ouch 是 Airtest 在 Android 端触控实现的抽象基类(BaseTouch),位于 airtest.core.android 的底层触控框架中。
  • 它定义了统一的触控接口和通用逻辑(坐标转换、方向适配、事件调度等),具体的触控驱动(如 Minitouch、Maxtouch、ADBTouch)都继承/实现这个基类。
  • 主要职责:
  1. 定义触控的标准操作:按下、移动、抬起、提交、等待等。
  2. 处理坐标相关的通用问题:屏幕分辨率、横竖屏旋转、触控坐标系与像素坐标的映射。
  3. 提供批量事件执行能力:把一组 MotionEvent 顺序下发到具体驱动。
  4. 管理驱动生命周期:安装/启动/停止底层触控服务(由子类具体实现)。

4.1、事件常用类

类名说明
MotionEvent由Minitouch/Maxtouch执行的运动事件
DownEvent(coordinates, contact=0, pressure=50)手指按下事件
UpEvent(contact=0) 手指抬起事件
MoveEvent(coordinates, contact=0, pressure=50)手指移动事件
SleepEvent(seconds) 等待事件

参数介绍:

  1. coordinates:二元组 (x, y),屏幕坐标(像素)。
  2. contact:手指编号/槽位,整数。默认 0。多指同时按下时使用不同的 contact(如 0、1)。
  3. pressure:压力值,通常为 0–100 的整数(默认 50)。很多设备会忽略该值,但保留设置不会有害。
  4. seconds:浮点数,单位:秒。

4.2、基础案例使用

导包:from airtest.core.android.touch_methods.base_touch import *

4.2.1、单指滑动

按下 --- 暂停 --- 滑动 --- 暂停 --- 抬起。

from airtest.core.api import *
from airtest.core.android.touch_methods.base_touch import *

connect_device("Android:///")
# 实现手指滑动操作
multitouch_event = [
    DownEvent((100, 1000), 0),  # 手指0按下(100, 1000)
    SleepEvent(1),
    MoveEvent((800, 1000), 0),  # 手指0移动到(800, 1000)
    SleepEvent(1),
    UpEvent(0)]  # 抬起手指0

# 执行操作
device().touch_proxy.perform(multitouch_event)

4.2.2、三指滑动

from airtest.core.api import *
from airtest.core.android.touch_methods.base_touch import *

# 定义三个手指并且按下
swipe_event = [DownEvent((100, 300), 0), DownEvent((100, 500), 1), DownEvent((100, 700), 2), SleepEvent(0.1)]
# 循环定义坐标,实现平滑移动
for i in range(5):
    swipe_event.append(MoveEvent((100 + 100*i, 300), 0))  # 第一根手指
    swipe_event.append(MoveEvent((100 + 100*i, 500), 1))  # 第二根手指
    swipe_event.append(MoveEvent((100 + 100*i, 700), 2))  # 第三根手指
    swipe_event.append(SleepEvent(0.2))
# 抬起三个手指
swipe_event.append(UpEvent(0))
swipe_event.append(UpEvent(1))
swipe_event.append(UpEvent(2))
# 执行操作
device().touch_proxy.perform(swipe_event)

五、录屏与报告增强

5.1、报告的全局配置

作者信息:__author__ = ""
报告标题:__title__ = ""
自定义描述:__desc__ = """ xxx"""
自定义步骤:assert_equal("1", "1", "这是1个断言")、log("这是1条log")
__author__ = "kkgg"
__title__ = "MyReport"
__desc__ = """这是我的第一个测试报告"""

assert_equal("1", "1", "这是1个断言")

log("这是1条log")

5.2、pycharm生成测试报告

5.2.1、生成测试报告

制作中...

注意:

  1. 生成Airtest报告需要依赖脚本运行过程保存的log,所以如需生成报告,就需要保存脚本运行过程的log,auto_setup 接口的logdir参数可以设置log保存路径;
  2. 使用simple_report生成报告时,就可以设置logpath,到logdir指定的log保存路径下面去找脚本对应的log内容。

5.3、生成多个测试报告

变量a
from airtest.report.report import simple_report
simple_report(__file__,logpath=True,output="log"+str(a)+".html")

# 注意每一次执行完脚本之后,回到首页并清理应用
keyevent("HOME")
clear_app("com.NetEase")

六、性能检测与日志

5.1、

adb = ADB(serialno=device().get_default_device())
print(adb.get_memory())  # 获取设备运行内存
print(adb.get_storage())   # 获取设备运行存储空间
print(adb.get_cpuinfo())  # 获取设备cup型号

七、最后

未完待续,会持续更新中....

本文参考了Airets官网的一些教程和总结,帮助大家更好的学习和总结。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐