Airtest+Poco 自动化测试全指南
一、引言:为什么选择 Airtest+Poco
1.1、工具定位与优势
Airtest:基于图像识别的跨平台自动化框架(支持 Android/iOS/Windows),适合动态 UI 场景(如游戏、不规则按钮)。
Poco:基于UI控件识别的定位工具,通过控件属性定位,适合静态 UI(如原生 APP、Unity/Cocos 游戏),它本质上也是 python 第三方库。组合价值:互补覆盖 90%+自动化场景,支持低代码开发(AirtestIDE),降低脚本维护成本。
注意:因为Airtest和poco都是基于python语法的,所以我们编写的测试脚本都应该符合python语法。
1.2、适用场景
- 移动 APP 自动化测试(功能测试、回归测试)。
- 游戏自动化(登录、任务流程、兼容性测试)。
- 跨设备批量操作(如多机型兼容性验证)。
1.3、前置准备
环境搭建:AirtestIDE 安装、Python依赖(pip install airtest pocoui)。
设备连接:Android(USB 调试)、iOS(证书配置)、模拟器设置。
1.3.1、连接Android手机
通过ADB连接你的电脑和Android手机,即可开始调试Android应用。ADB是Google官方提供的Android调试工具。AirtestIDE依赖ADB与安卓设备进行通信。
打开AirtestIDE,按照以下步骤进行连接:
- 打开手机设置-开发者选项-USB调试开关;
- 在AirtestIDE设备面板中点击refresh ADB按钮,查看连接上的设备;
- 如果没有显示出设备,试试restart ADB;
- 能够成功看到设备后,点击对应设备的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可以将被测窗口嵌入,方便脚本录制和调试。
- 在AirtestIDE设备面板中点击选择游戏画面按钮;
- 将鼠标移动到被测程序的窗口上,会显示红色边框,将对应的窗口框出;
- 单击鼠标左键即可将对应的窗口嵌入到AirtestIDE中;
- 也可以点击搜索窗口按钮,选择对应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¶m2=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/设备ID | Android://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.00,则输出版本号为100。
- 第一条命令中的APK_PATH,传入的是PC上apk的路径。
- 第二条命令中的PACKAGE,传入是的包名。
# 获取本地apk文件的版本号
apk_version = int(APK(APK_PATH).androidversion_code)
# 获取手机已安装包体的版本号
installed_version = android.adb.get_package_version(PACKAGE)
检测是否需要执行覆盖安装的思路大概是:
- 获取本地apk的版本号(即准备安装到手机上的那个包体) ;
- 获取手机里已经安装好的包体的版本号;
- 对比获得的2个版本号,如果本地apk版本号大于手机里包体的版本号,则判断需要执行覆盖安装。
关于“安装包体”的几个小知识点:
- 检查package在设备中是否存在:check_app(package)
- 打印出package的完整路径:path_app(package)
- 获取本地apk文件的版本号:int(APK(apk_path).androidversion_code)
- 获取手机中包体的版本号:Android().adb.get_package_version(package)
- 覆盖安装: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)
- filepath:apk文件在PC上的完整路径。
- replace: 如果应用已存在,是否替换,默认为False。
- install_options:install 命令的额外选项,默认是[],可填入 "-l"、"-t"、"-s"、"-d"和"-g" 等参数,用于控制安装apk的行为。
2.1.2.3、启动app
start_app(package, activity=None)
- 启动应用。
- 参数:
- package – 包名。
- activity – 界面名。
2.1.4.4、启动app,返回启动时间
源码
start_app_timing(package, activity)
- 启动应用,并且返回启动耗费时间
- 参数:
- package – 包名。
- 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编辑器窗口
- 一些PC游戏只接收来自底层硬件层面的发送的键盘扫描码,而不响应例如keyevent()所用的pywinauto库发送的操作系统层面的虚拟键码,所以我们提供了新的接口,key_press(key)和key_release(key),来通过发送键盘扫描码进行键盘按键按下和释放的模拟。
- 一些游戏要求在按键按下和释放之间进行一定的操作,因此我们将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。
- 参数:
- filename – 保存截图的文件名,默认为None的话将会发送到stdout。
- ensure_orientation – 截图方向是否要与当前显示情况一致,默认为True。
- quality – 图像质量,整数范围为[1, 99]。
- 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()
- 唤醒并解锁目标设备。
- 在部分品牌手机上可能无法生效。
wake()
dev.wake()
"""
支持平台:Android
"""
2.1.3.5、返回home界面 - home()
返回HOME界面。
home()
dev.home()
"""
支持平台:Android, iOS
"""
2.1.3.5、录屏操作
- Airtest所提供的录屏方法,目前该录屏方法已经兼容了Android、iOS、Windows上的脚本跑测录制。
- 下面这个两个方法,对于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 :原有的默认模式,录屏清晰度和质量较高,但兼容性较差,部分机型可能有问题。
- 支持的参数:max_time, orientation, bit_rate, bit_rate_level。
- ffmpeg : 在Airtest 1.2.9版本之后加入的新模式,录屏帧率和质量较低,但兼容性更好,能兼容绝大多数设备。
- 支持的参数: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")
总结
- 先设置允许所以的权限,再启动app。
- 我们在同意了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)
- 在当前设备画面上进行一次点击。
- 参数:
- v – 点击位置,可以是一个Template图片实例,或是一个绝对坐标 (x, y)
- times – 点击次数。
- duration - 点击持续时间。
- kwargs – 平台相关的参数kwargs,请参考对应的平台接口文档。
案例
touch(Template(r"tpl1638179990578.png", record_pos=(0.179, -0.57), resolution=(810, 1440)))
- Template:图像模板类,自动生成。
- 参数1:rgb:是否启用 RGB 三通道颜色匹配,默认False。
- 参数2:record_pos:截图时目标图像相对于屏幕中心的相对位置(坐标范围:-1 ~ 1)。
- 参数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)
- 在当前设备画面上进行一次滑动操作。
- 有两种传入参数的方式:
- swipe(v1, v2=Template(...)),从 v1 滑动到 v2。
- swipe(v1, vector=(x, y)),从 v1 开始滑动,沿着vector方向。
- 参数:
- v1 – 滑动的起点,可以是一个Template图片实例,或是绝对坐标 (x, y)。
- v2 – 滑动的终点,可以是一个Template图片实例,或是绝对坐标 (x, y)。
- vector – 滑动动作的矢量坐标,可以是绝对坐标 (x,y) 或是屏幕百分比,例如 (0.5, 0.5)。
- **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。
- 参数:
- coordinates_list – 一个坐标的列表:[(x1, y1), (x2, y2), (x3, y3)]。
- duration – 滑动过程中的时间间隔,默认是0.8。
- 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捏合操作。
- 参数:
- in_or_out - 向内捏合或向外扩大,在[“in",“out"]中枚举一个值。
- center - pinch动作的中心位置,默认值为None则为屏幕中心点。
- 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。
- 参数:
- tuple_from_xy - 开始坐标(元组)。
- tuple_to_xy - 结束坐标(元组)。
- duration - 滑动持续时间的时间间隔,默认值为0.8。
- steps - 滑动步长,默认值为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()
- drop_to(start, end,duration)。
- 拖拽操作(从 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)
- 在目标设备上输入文本,文本框需要处于激活状态。
- 参数:
- text – 要输入的文本。
- enter – 是否在输入完毕后,执行一次 Enter ,默认是True。
- 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图片。
- 参数:
- v – 要等待出现的目标Template实例。
- timeout – 等待匹配的最大超时时长,默认为None即默认取 ST.FIND_TIMEOUT的值。
- interval – 尝试查找匹配项的时间间隔(以秒为单位)。
- 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)
- 检查设备上是否存在给定目标。
- 参数:v – 要检查的目标。
- 返回:如果未找到目标,则返回False,否则返回目标的坐标。
- 支持平台: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)
在设备屏幕上查找所有出现的目标并返回其坐标列表。
参数:
- v:寻找目标。
- 返回:结果列表。
- 支持平台: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)
案例
- cv2.resize 用于统一图片尺寸,上图中的(85,100)为我们指定的目标尺寸。
- cv2.imread用于读取本地路径下的图片。
- 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)
- 断言成功,不会打印后面的信息;断言失败,会打印后面的提示信息。
- eval()用于将字符串解析为可执行代码并返回运算结果,执行错误时抛出异常。
- snapshot参数,默认为snapshot=True,为了在设置断言时,还能附带截取当前画面的图片,然后显示在Airtest报告中。
案例
2.3.4、断言值不相等 - assert_not_equal()
源码
assert_not_equal(first, second, msg="", snapshot=True)
- 断言成功,不会打印后面的信息;断言失败,会打印后面的提示信息。
- 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)
- 在屏幕中搜索图像模板,直到超时。
- 参数:
- query:要在屏幕截图中查找的图像模板。
- timeout:查找图像模板的时间间隔。
- threshold:阈值,默认为None。
- interval:下次尝试查找图像模板前的休眠间隔。
- 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)
- 将屏幕截图保存到文件。
- 参数:
- screen:待保存的屏幕截图,默认路径 ST.LOG_DIR。
- quality:图像质量,默认值为 ST.SNAPSHOT_QUALITY。
- max_size:图片的最大尺寸,例如1200。
- 返回:返回一个字典,包含图片名称和大小。例如{'screen': '1766560922736.jpg', 'resolution': (261, 281)}。
2.5.3、局部找图 - match_in()
源码
Template(object).match_in(screen)
- 在指定的图片对象中找图。
- 参数:
- local_screen:待查找的局部图片,可以传入图片路径。
- 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("确认已进入到游戏的开始界面")
- 代码逻辑的核心在于快速地识别目标图像。
- 首先,将目标图像(最好是特征鲜明、尺寸小一些)列表输入Multiple_exists()函数。该函数通过循环执行截图操作,每0.2秒进行一次,以最小化循环识别时间。
- 接着将设备屏幕截图和目标图像传递给match_in_predict_area()函数,进行裁剪和搜索。
- 一旦找到匹配的图像,立即将坐标信息反馈给Multiple_exists()函数,并最终将图像编号和位置信息返回至主函数,供进一步使用。
2.6.2、Airtest结合Poco对控件实施精准截图
- 要实现“通过选择器获取到了控件,然后截图这个控件范围”,根据我们的Airtest的局部截图接口以及poco控件的属性查询接口是可以很快实现的。
- 可以用于在手机桌面上截取app的icon。
- 实现:
- 截取设备的整个屏幕:G.DEVICE.snapshot()
- 将控件的局部坐标转换成全局坐标。
- 获取局部截图: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类(单例实例)
- G 是Airtest内置的全局上下文对象(来自 airtest.core.api import G),本质是一个封装了全局变量的命名空间,用于存储整个测试会话中“跨模块/跨函数共享”的核心资源。
- 简单说: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 = {}
| 属性名 | 类型 | 作用说明 |
| BASEDIR | list | 存储脚本的基础目录(可多目录),用于查找截图模板、配置文件等。 |
| LOGGER | AirtestLogger | Airtest 封装的日志器(核心),负责记录步骤、生成报告、输出彩色日志。 |
| LOGGING | logging.Logger | Python 标准库日志器,用于 Airtest 内部底层日志输出(区别于业务日志)。 |
| SCREEN | Screen/None | 屏幕捕捉器实例(不同设备有不同实现:AndroidScreen/WindowsScreen)。 snapshot() 底层调用 G.SCREEN.snapshot() 实现截屏。 |
| _DEVICE(私有) | Device/None | 存储当前活跃的设备实例(Android/iOS/Windows)。 元类映射到 G.DEVICE,开发者无需直接访问。 |
| DEVICE(元类映射) | Device/None | 对外暴露的 “当前设备实例”,所有设备操作的核心入口 |
| DEVICE_LIST | list | 存储所有已连接的设备实例(多设备场景)。 |
| RECENT_CAPTURE | PIL.Image/None | 存储最近一次截屏的图像数据(PIL 对象),用于断言、二次处理。 |
| RECENT_CAPTURE_PATH | str/None | 存储最近一次截屏的文件路径,生成报告时自动关联该路径。 |
| CUSTOM_DEVICES | dict | 注册自定义设备类型(扩展 Airtest 支持的设备)。 |
2.7.2、G.DEVICE
- G.DEVICE 赋值时机:
- 只有通过 auto_setup()、connect_device() 成功连接设备后,G.DEVICE 才会被赋值。
- 未连接设备时调用 G.DEVICE 会抛出 AttributeError(提示 'NoneType' object has no attribute 'xxx')。
- 多设备场景的坑:
- 若未用 set_current() 切换设备,G.DEVICE 始终指向 “第一个连接的设备”。
- 多设备操作时,务必先 set_current(dev) 再调用 G.DEVICE,避免操作错设备。
- 与 Poco 的关系:
- Poco 初始化时会自动绑定 G.DEVICE(当前设备),因此 Poco 的控件操作(如 poco("btn").click())最终也会通过 G.DEVICE 执行底层点击。
G.DEVICE.display_info[ ]
display_info[ ]:设备的显示信息字典,由Airtest从设备底层(如Android的ADB命令、Windows的系统 API)获取,包含屏幕分辨率、旋转方向、像素密度等关键参数。
orientation的值用于判断横竖屏
- display_info['orientation']
- display_info字典中键为orientation的值,代表屏幕的旋转方向/角度,常见取值为:
| 返回值 | 安卓/iOS | Windows |
| 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()
说明:大部分方法底层都是直接调用上面进行执行
- touch() 底层调用 G.DEVICE.touch(pos, **kwargs)。
- snapshot 底层调用 G.DEVICE.snapshot(filename, quality=quality, max_size=max_size)。
- ........
总结
G.DEVICE 是 Airtest 对 “当前活跃设备” 的全局底层引用:
- 对普通用户:日常用快捷 API(touch()、snapshot())即可,无需直接操作 G.DEVICE。
- 对进阶用户:当需要获取设备底层信息、调用未封装的设备功能、多设备精准控制时,G.DEVICE 是核心入口。
2.7.3、ST类
Settings是Airtest内置的全局配置类,源码中会被实例化为单例 ST(from airtest.core.settings import ST),作用是:
- 统一存储自动化运行的所有核心参数(无需零散定义全局变量);
- 提供合理的默认值,兼顾通用性和稳定性;
- 支持动态修改(全局/局部),适配不同测试场景(如调整图像匹配精度、超时时间);
- 兼容环境变量和版本差异(如 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、调试与日志配置
| 属性 | 默认值 | 作用 |
| DEBUG | false | 调试模式开关:开启后输出更详细的底层日志(如图像匹配的关键点、耗时) |
| LOG_DIR | None | 日志存储目录:默认随脚本目录,设置后日志 / 截屏会存入该目录 |
| 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、超时与操作延迟配置
| 属性 | 初始值 | 作用 |
| OPDELAY | 0.1 | 每次操作(touch/swipe/text)后的延迟时间(秒): 避免操作过快导致设备无响应。 |
| FIND_TIMEOUT | 20 | 全局查找元素(图像 / 控件)的超时时间(秒): wait()/exists() 等默认使用。 |
| FIND_TIMEOUT_TMP | 3 | 临时查找超时(如模板匹配的快速校验):用于非核心步骤的快速判断。 |
2.7.3.4、路径与环境配置
| 属性 | 初始值 | 作用 |
| PROJECT_ROOT | 读取环境变量PROJECT_ROOT,默认空。 | 项目根路径:使用 using() 导入其他脚本时,会从该路径查找脚本 / 模板。 |
2.7.3.5、截屏相关配置
| 属性 | 初始值 | 作用 |
| SNAPSHOT_QUALITY | 10(1-100) | 截屏压缩质量:数值越大画质越好(100为无损),文件体积也越大。 |
| IMAGE_MAXSIZE | 读取环境变量IMAGE_MAXSIZE,默认None | 截屏最大尺寸限制:如设为1200,则截屏宽/高不超过1200px(等比缩放)。 |
| SAVE_IMAGE | True | 是否保存截屏文件:关闭后仅内存缓存,不写入磁盘。 |
三、Poco UI定位与操作
Poco是一款基于Python语法的、与引擎无关的自动化测试框架,它基于控件搜索原理,可以准确定位到应用里面的控件,并且对控件进行点击、滑动等操作。
class Poco(agent, **options)
- Agent(PocoAgent)– Poco用于与目标设备通信的代理对象。
- options -
- action_interval:等待在设备上执行的操作(如触摸或滑动)完成以及UI变得静止(稳定)所需的时间间隔。默认值为0.8秒。
- poll_interval:每次轮询事件(例如等待 UI 元素出现在屏幕上)之间所需的最短时间。轮询是定期进行的。
- pre_action_wait_for_appearance:执行操作(如点击或滑动)前等待的时间间隔。如果在此时间间隔后目标 UI 元素未出现在屏幕上,则会抛出 PocoNoSuchNodeException 异常。
- touch_down_duration:点击操作按下步骤的持续时间。如果提供此参数,该值将被设置到 self.agent.input 模块中。请注意,并非所有 poco 的实现都支持此参数。如果不支持,你可能会看到一条警告信息。
- 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)
- use_airtest_input=True是指,使用Airtest去执行点击操作,好处是,会在日志里面记录一条log记录,这样生成报告时,就能在报告上显示这个点击记录。
- screenshot_each_action=False是指,是不开启每个poco步骤的截图。
- 至于,为什么当初始化时,指定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种定位选择器分别是:
- 基本选择器
- 相对选择器
- 空间顺序选择器
3.2.1、基本选择器(直接/正则)
- 在poco实例后加一对括号,括号里的参数就是所给定的条件,比如节点的属性及预期的属性值。选择器会遍历所有元素,将满足给定条件的元素都选出来并返回。
- 常用的属性有name、text、type、tag等,属性默认值为name。
定位控件
- 在poco实例后加一对括号,我们就可以进行元素选择了。选择器会遍历所有元素,将满足给定条件的元素都选出来并返回。
- 括号里的参数就是所给定的条件,用属性名值对表示,其中第一个参数表示 节点名 ,就像 poco("star_single") 。
- 后面还可以跟着一些可选参数,均表示节点的属性及预期的属性值。
poco("star_single",type="Image")
利用正则表达式匹配控件
使用方法:poco(xxMatches="预期属性值的正则表达式")
- textMatches(),使用text的正则表达式进行匹配。
- nameMatches(),使用name的正则表达式进行匹配。
- 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、空间顺序选择器(位置)
- 若寻找的元素存在大量的重名,没有办法准确定位时,就采用空间顺序选择器。
- 先定位到元素控件名称相同的一批元素,然后通过下标去定位要访问的元素。
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()
- 先使用poco定位到元素,再进行点击操作。
- 定位可以通过元素的: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()
- 先使用poco定位到元素,再进行点击操作。
- 定位可以通过元素的:name(默认)、type。
- 默认长按2s,可以传入参数,设定时间。
# 默认长按2s
poco("blue").long_click()
# 可以传入参数,设定时间
poco("blue").long_click(10)
3.3.3、双击元素 - double_click()
- 先使用poco定位到元素,再进行点击操作。
- 定位可以通过元素的:name(默认)、type。
# 双击
poco("blue").double_click()
3.3.4、指定基准点 - focus()
- 可以通过focus来定位需要操作的位置。
- [0,0]表示左上角,[0.5,0.5]表示中心点,[1,1]表示右下角。
poco("blue").focus([0.5,0.5]).click()
cilck()与focus()使用上的区别:
- click()的操作,只能对当前的语句生效,下面其他的就还是默认值。
- focus()如果一旦设定了,后面使用当前控件去操作,会保留focus的设定。
blue_btn = poco("blue").focus([1,1]) # 点击右下角
blue_btn.click()
3.3.5、滑动元素 - swipe()
定位元素进行滑动
- 先定位一个元素,然后选择方向移动。
- 参数 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) # 向右
按照屏幕比例进行滑动
- poco.swip([x1,y1],[x2,y2],duration),按照比例滑动,归一化坐标。
- 参数 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()
- 从一个元素拖拽到另一个元素上面。
- 参数 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()
- UI自动化的用例中,有时候需要频繁的操作UI,因为poco的所有操作,几乎每次操作前,都要通过发送rpc请求把App的UI树从手机拉到PC,如属性类操作attr()、各种等待、各种点击等,这将非常耗费时间。这么做的原因是Poco无法保证App界面有没有发生变化,所以每次操作前都要去获取实时的UI树。
- 所以Poco提供了freeze()方法,它把UI树拉取一次后,保存在PC内存中,以后再有交互操作时,不再去手机拉取,而是直接读取PC内存中保存的这份UI树。这将极大提高操作效率从而节省UI自动化执行时间。
- 不过这种方法有一个缺点,就是在你这段操作时间内,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()
- 仅等待不报错。
- 先定位到某个元素,等待设定的时间后,再进行其他的操作。
注意: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()
- 等待,不满足则报错。
- 轮询查找,找到/找不到就结束轮询。
- Poco控件还支持另外2个等待事件,wait_for_appearance()和wait_for_disappearance()。这两个API可以帮助我们等待页面上某1个UI出现或者消失,等待的超时时间timeout默认为120秒。
- 如果在超时时长之内元素没有出现或者消失的话,会报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控件。
设置:
- 打开微信中任意一个聊天框,输入 debugtbs.qq.com,点击进入TBS调试页面,再点击页面内的“安装线上内核”;
- 安装成功后,我们需要重启微信才可以生效;
- 再次进去刚才的TBS调试页面,检查我们的设置是否生效,点击“查看版本信息”,版本信息为 0(null) 则设置成功;
- 设置成功以后即可用Poco对小程序进行测试。
Poco测试小程序实操
注意
- TBS调试只适用于Android系统,iOS暂不支持。
- 在小程序测试的实操中,我们可以灵活地交替使用Airtest和Poco脚本,比如:有些编辑框无法使用Poco的set_text接口,我们可以先使用poco进行点击操作,再用Airtest的text接口。
3.7、Poco常用技巧
3.7.1、提高遍历pocoUI树的效率
在Poco的实际应用过程中,我们可能会经常遇到需要遍历pocoUI树的情况,整个项目的UI树,体积有可能非常庞大。
- 方法一:offspring(),每次对poco实例的属性进行操作,都需要重新生成一个dump,这非常耗费资源。 除非在之后的业务逻辑里要对offspring()进行不可同步的操作,否则不建议采取这种方法。
- 方法二:offspring("参数"),offspring()方法里跟指定参数,可以将dump操作缩减为1次,大大提升效率。
- 方法三:freeze(),一次抓取(消耗几百毫秒),可以使用多次,freeze()的效率极高,但需要手动刷新。
3.7.2、swipe的优化
- 在使用过程中,我们会看见有两个swipe,一个是Airtest中的swpie,另一个是poco.swpie。
- 推荐使用:Airtest中的swipe+获取设备的分辨率*百分比。
- 不太推荐使用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)都继承/实现这个基类。
- 主要职责:
- 定义触控的标准操作:按下、移动、抬起、提交、等待等。
- 处理坐标相关的通用问题:屏幕分辨率、横竖屏旋转、触控坐标系与像素坐标的映射。
- 提供批量事件执行能力:把一组 MotionEvent 顺序下发到具体驱动。
- 管理驱动生命周期:安装/启动/停止底层触控服务(由子类具体实现)。
4.1、事件常用类
| 类名 | 说明 |
| MotionEvent | 由Minitouch/Maxtouch执行的运动事件 |
| DownEvent(coordinates, contact=0, pressure=50) | 手指按下事件 |
| UpEvent(contact=0) | 手指抬起事件 |
| MoveEvent(coordinates, contact=0, pressure=50) | 手指移动事件 |
| SleepEvent(seconds) | 等待事件 |
参数介绍:
- coordinates:二元组 (x, y),屏幕坐标(像素)。
- contact:手指编号/槽位,整数。默认 0。多指同时按下时使用不同的 contact(如 0、1)。
- pressure:压力值,通常为 0–100 的整数(默认 50)。很多设备会忽略该值,但保留设置不会有害。
- 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、生成测试报告
制作中...
注意:
- 生成Airtest报告需要依赖脚本运行过程保存的log,所以如需生成报告,就需要保存脚本运行过程的log,auto_setup 接口的logdir参数可以设置log保存路径;
- 使用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官网的一些教程和总结,帮助大家更好的学习和总结。
更多推荐










所有评论(0)