airtest 安装
airtest 常用api

airtest是一个跨平台的、基于图像识别的UI自动化测试框架,适用于游戏和App,支持平台有Windows、Android和iOS

安装airtest


创建python虚拟环境并安装

pip install airtest

安装问题

Could not find a version that satisfies the requirement opencv-contrib-python<=3.4.2.17 (from airtest) (from versions: 3.4.8.29, 4.1.2.30...)。
这是因为Airtest 使用了 opencv-contrib-python 作为依赖且版本需要 <=3.4.2.17,而opencv-contrib-python-3.4.2.17 不支持 python 3.8及以上版本。在官方未提供支持前,解决方案是创建<=python3.7的虚拟环境

添加airtest使用的adb的执行权限

permissionError: [Errno 13] Permission denied: '~/.pyenv/versions/3.6.8/lib/python3.6/site-packages/airtest/core/android/static/adb/mac/adb'
airtest使用的是自身的adb,在安装包的如上目录中,在Mac/Linux系统下,需要手动赋予该adb可执行权限,否则可能在执行脚本时遇到 Permission denied.

chmod +x adb

官方文档


常用api及示例


以下以android为例

device相关操作

  • get_default_device:获取默认 device
  • uuid:获取当前 Device 的 UUID
  • list_app:列举所有 App
  • path_app:打印输出某个 App 的完整路径
  • check_app:检查某个 App 是否在当前设备上
  • start_app:启动某个 App
  • start_app_timing:启动某个 App,然后计算时间
  • stop_app:停止某个 App
  • clear_app:清空某个 App 的全部数据
  • install_app:安装某个 App
  • install_multiple_app:安装多个 App
  • uninstall_app:卸载某个 App
  • snapshot:屏幕截图
  • shell:获取 Adb Shell 执行的结果
  • keyevent:执行键盘操作
  • wake:唤醒当前设备
  • home:点击 HOME 键
  • text:向设备输入内容
  • touch:点击屏幕某处的位置
  • double_click:双击屏幕某处的位置
  • swipe:滑动屏幕,由一点到另外一点
  • pinch:手指捏和操作
  • logcat:日志记录操作
  • getprop:获取某个特定属性的值
  • get_ip_address:获取 IP 地址
  • get_top_activity:获取当前 Activity
  • get_top_activity_name_and_pid:获取当前 Activity 的名称和进程号
  • get_top_activity_name:获取当前 Activity 的名称
  • is_keyboard_shown:判断当前键盘是否出现了
  • is_locked:设备是否锁定了
  • unlock:解锁设备
  • display_info:获取当前显示信息,如屏幕宽高等
  • get_display_info:同 display_info
  • get_current_resolution:获取当前设备分辨率
  • get_render_resolution:获取当前渲染分辨率
  • start_recording:开始录制
  • stop_recording:结束录制
  • adjust_all_screen:调整屏幕适配分辨率
from airtest.core.android import Android
from airtest.core.api import *
import logging

logging.getLogger("airtest").setLevel(logging.WARNING)

device: Android = init_device('Android')

ip_address = device.get_ip_address()
print(0, f'ip_address: {ip_address}')

is_locked = device.is_locked()
print(1, f'is_locked: {is_locked}')

if is_locked:
    device.unlock()

device.wake()
default_device = device.get_default_device()
print(2, f'default_device: {default_device}')

uuid = device.uuid
print(3, f'uuid: {uuid}')

app_list = device.list_app()
print(4, f'app list: {app_list}')

# app_path = device.path_app(app_list[0])
# print(5, f'app path: {app_path}')

# display_info: dict = device.display_info
display_info: dict = device.get_display_info()
print(5, f'display info: {display_info}',
      f'width and height: {display_info.get("width")}, {display_info.get("height")}')

current_resolution = device.get_current_resolution()
print(6, f'current_resolution: {current_resolution}')

render_resolution = device.get_render_resolution()
print(7, f'render_resolution: {render_resolution}')

is_keyboard_shown = device.is_keyboard_shown()
print(8, f'is keyboard shown: {is_keyboard_shown}')

package_name = 'com.android.dazhihui'
apk_path = 'test.apk'
is_app_installed = False
try:
    is_app_installed = device.check_app(package_name)
except:
    pass

if is_app_installed:
    print(9, is_app_installed)
    print(10, f'app path: {device.path_app(package_name)}')
    # device.clear_app(package_name)
    device.stop_app(package_name)
    device.start_app(package_name)
    time.sleep(5)
    top_activity = device.get_top_activity()
    top_activity_name_and_pid = device.get_top_activity_name_and_pid()
    top_activity_name = device.get_top_activity_name()
    print(11, f'top activity related: {top_activity}, {top_activity_name_and_pid}, {top_activity_name}')
    # device.uninstall_app(package_name)
else:
    device.install_app(apk_path, replace=True)

device.home()
current_resolution_width = current_resolution[0]
current_resolution_height = current_resolution[1]
device.swipe((current_resolution_width/5, current_resolution_height/2), (current_resolution_width, current_resolution_height/2))

touch(Template('seach.png')
text("wechat",enter=False,search=True) # 默认情况下,text是带回车的,不需要可以传入False;安卓平台下,还支持输入后点击软键盘的搜索按钮

对install_app的补充说明

  • install_app(filepath,replace=False, install_options=None)
    • filepath:apk文件在PC上的完整路径
    • replace:如果应用已存在,是否替换,默认为False
    • install_options:install 命令的额外选项,默认是None,可填入 "-l"、"-t"、"-s"、"-d"和"-g" 等参数,用于控制安装apk的行为。多个参数时,可用list传入,如['-d', '-g']
  • 其中,install_options 的各参数的含义如下:
    • "-l" ,将应用安装到保护目录/mnt/asec
    • "-t" ,允许安装AndroidManifest.xml里application指定android:testOnly="true"的应用
    • "-s" ,将应用安装到sdcard
    • "-d" ,允许降级覆盖安装
    • "-g" ,授予所有运行时权限

device处理

  1. 初始化设备
    如果设备没有被初始化的话会进行初始化,并把初始化的设备作为当前设备,源码如下

    def init_device(platform="Android", uuid=None, **kwargs):
        """
        Initialize device if not yet, and set as current device.
    
        :param platform: Android, IOS or Windows
        :param uuid: uuid for target device, e.g. serialno for Android, handle for Windows, uuid for iOS
        :param kwargs: Optional platform specific keyword args, e.g. `cap_method=JAVACAP` for Android
        :return: device instance
        """
        cls = import_device_cls(platform)
        dev = cls(uuid, **kwargs)
        # Add device instance in G and set as current device.
        G.add_device(dev)
        return dev
    
  2. 获取当前设备

    from airtest.core.android import Android
    from airtest.core.api import *
    import logging
    logging.getLogger("airtest").setLevel(logging.WARNING)
    
    device_init: Android = init_device('Android')
    print(device_init)
    print(device()) # 获取当前 device
    

    可以发现它返回的是一个 Android 对象。 这个 Android 对象实际上属于 airtest.core.android 这个包,继承自 airtest.core.device.Device 这个类,与之并列的还有 airtest.core.ios.ios.IOS 、airtest.core.linux.linux.Linux、airtest.core.win.win.Windows 等

  3. 关于设备字符串
    以安卓为例, 字串完整定义如下:

    Android://<adbhost[localhost]>:<adbport[5037]>/<serialno>
    

    adbhost是adb server所在主机的ip,默认是本机127.0.0.1,adb port默认是5037,serialno是android手机的序列号

    # 什么都不填写,会默认取当前连接中的第一台手机
    Android:///
    # 连接本机默认端口连的一台设备号为79d03fa的手机
    Android://127.0.0.1:5037/79d03fa
    # 用本机的adb连接一台adb connect过的远程设备,注意10.254.60.1:5555其实是serialno
    Android://127.0.0.1:5037/10.254.60.1:5555
    # 连接一个Windows窗口,窗口句柄为123456
    Windows:///123456
    # 连接一个Windows窗口,窗口名称匹配某个正则表达式
    Windows:///?title_re=Unity.*
    # 连接windows桌面,不指定任何窗口
    Windows:///
    # 连接iOS手机
    iOS:///127.0.0.1:8100
    
  4. 连接设备
    源码如下:(其实内部就是初始化设备init_device,返回设备对象)

    def connect_device(uri):
        """
        Initialize device with uri, and set as current device.
    
        :param uri: an URI where to connect to device, e.g. `android://adbhost:adbport/serialno?param=value&param2=value2`
        :return: device instance
        :Example:
            * ``android:///`` # local adb device using default params
            * ``android://adbhost:adbport/1234566?cap_method=javacap&touch_method=adb``  # remote device using custom params
            * ``windows:///`` # local Windows application
            * ``ios:///`` # iOS device
        """
        d = urlparse(uri)
        platform = d.scheme
        host = d.netloc
        uuid = d.path.lstrip("/")
        params = dict(parse_qsl(d.query))
        if host:
            params["host"] = host.split(":")
        dev = init_device(platform, uuid, **params)   # 直接连接,其实内部也进行了初始化
        return dev
    

    举例:

    from airtest.core.android import Android
    from airtest.core.api import *
    import logging
    logging.getLogger("airtest").setLevel(logging.WARNING)
    
    uri = 'Android://127.0.0.1:5037/9A201FFAZ0007D'
    device: Android = connect_device(uri)
    print(device)
    
  5. 获取所有设备
    在 airtest 中有一个全局类 G,类属性DEVICE_LIST用于存储所有连接的设备

    from airtest.core.android import Android
    from airtest.core.api import *
    import logging
    logging.getLogger("airtest").setLevel(logging.WARNING)
    
    uuid = ['9A201FFAZ0007D', 'FJH5T18A10012174']
    for i in uuid:
        connect_device(f'Android://127.0.0.1:5037/{i}')
    print(G.DEVICE_LIST)
    

    最开始没有调用 connect_device 方法之前,DEVICE_LIST 是空的,在调用之后 DEVICE_LIST 会自动添加已经连接的 device,DEVICE_LIST 就是已经连接的 device 列表

  6. 切换设备

    from airtest.core.android import Android
    from airtest.core.api import *
    import logging
    
    logging.getLogger("airtest").setLevel(logging.WARNING)
    
    uuid = ['9A201FFAZ0007D', 'FJH5T18A10012174']
    for i in uuid:
        connect_device(f'Android://127.0.0.1:5037/{i}')
    print(G.DEVICE_LIST)
    print(device())  # 默认是最后一个
    set_current(uuid[0])   # idx: uuid or index of initialized device instance
    print(device())
    

在设备上执行命令

shell源码如下:

@logwrap
def shell(cmd):
    """
    Start remote shell in the target device and execute the command

    :param cmd: command to be run on device, e.g. "ls /data/local/tmp"
    :return: the output of the shell cmd
    :platforms: Android
    """
    return G.DEVICE.shell(cmd)

示例(获取内存信息就可以使用如下命令):

from airtest.core.api import *
import logging

logging.getLogger("airtest").setLevel(logging.WARNING)

uri = 'Android://127.0.0.1:5037/9A201FFAZ0007D'
connect_device(uri)

result = shell('cat /proc/meminfo')
print(result)

其他操作

上面举例了swipe和home操作,这里再补充介绍下snapshot,touch,pinch,keyevent

  1. snapshot(filename=None, msg="", quality=None, max_size=None)
    from airtest.core.api import *
    
    uri = 'Android://127.0.0.1:5037/emulator-5554'
    connect_device(uri)
    
    package = 'com.tencent.mm'
    start_app(package)
    sleep(3)
    snapshot('weixin.png', quality=30)
    
  2. touch(v, times=1, **kwargs)/clickdouble_click(v)
    点击,可以传入一张图或者绝对位置,同时可以指定点击次数。touch 方法完全等同于 click 方法。如果要双击的话,还可以使用调用 double_click 方法,传入参数也可以是 Template 或者绝对位置
  3. keyevent(keyname, **kwargs)
    keyevent 来输入某个键,例如 home、back 等等
  4. pinch(in_or_out='in', center=None, percent=0.5)
    放大缩小是使用的 pinch 方法,可以指定放大还是缩小,同时还可以指定中心位置点和放大缩小的比率
  5. 示例
    from airtest.core.api import *
    import logging
    
    logging.getLogger("airtest").setLevel(logging.WARNING)
    
    uri = 'Android://127.0.0.1:5037/9A201FFAZ0007D'
    connect_device(uri)
    
    # touch(Template('tpl.png'))
    click(Template('tpl.png'))
    home()
    touch((400, 600), times=2)
    keyevent('HOME')
    double_click((400, 600))
    keyevent('BACK')
    keyevent('BACK')
    pinch(in_or_out='out', center=(300, 300), percent=0.4)
    
  6. 其他
    根据实际情况查看源码调用即可,这里不再举例说明,可参看1个模拟各种复杂的滑动或手势操作的方法
    • device.two_finger_swipe()
    • device.swipe_along()
    • device.maxtouc()
    • device.minitouch()
    • device.touch_proxy()

等待和判断

  • 可以使用 wait 方法等待某个内容加载出来,需要传入的是 Template,返回coordinates of the matched target。常用于等待某一张图片出来之后,再进行下一步操作,可以传入等待的超时时长、查找的时间间隔和首次尝试查找失败的回调函数
  • 也可以使用 exists 方法判断某个内容是否存在
from airtest.core.api import *
import logging

logging.getLogger("airtest").setLevel(logging.WARNING)

def test():
    print("未等待到目标")

uri = 'Android://127.0.0.1:5037/9A201FFAZ0007D'
device = connect_device(uri)

resolution = device.get_current_resolution()
if not exists(Template('tpl.png')):
    swipe((resolution[0]/2, resolution[1]*3/4), (resolution[0]/2, resolution[1]/4))
else:
    touch(Template('tpl.png')

wait(Template(r"tpl.png", record_pos=(-0.036, -0.189), resolution=(1080, 1920)),timeout=120,interval=3,intervalfunc=test())

查找目标find_all()和loop_find()

  • find_all()在设备屏幕上查找所有出现的目标并返回其坐标列表
  • loop_find()循环查找,查看源码可发现touch()、wait()和exists()内部都调用了loop_find()

断言

  • assert_exists 、assert_not_exists、assert_equal、assert_not_equal
  • assert_exists和assert_not_exists,判断某个目标是否存在于屏幕上,同时还可以传入 msg,它可以被记录到 report 里面

查看源码查找目标loop_find(v, timeout=ST.FIND_TIMEOUT, threshold=ST.THRESHOLD_STRICT or v.threshold),超时时间和阙值都是全局变量,timeout为20s,threasold为0.7,所以脚本只有在20s内找到置信度>0.7的结果,断言才会成功

  • assert_equal和assert_not_equal,判断传入的两个值是否相等,同时还可以传入 msg,它可以被记录到 report 里面
assert_exists(Template('tpl.png'), msg='该目标存在')

# assert_equal("实际值", "预测值", "目标值正确"),以下代码中value通过控件属性获取
assert_equal(value, "8", "按钮值为8")

更多