0%

Airtest 和 Poco 的 API 总结

Airtest

首先需要安装 Airtest,使用 pip3 即可:

1
pip3 install airtest

初始化 device

如果设备没有被初始化的话会进行初始化,并把初始化的设备作为当前设备。 用法如下:

1
2
3
4
5
6
7
8
9
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
"""

示例如下:

1
2
device = init_device('Android')
print(device)

运行结果如下:

1
<airtest.core.android.android.Android object at 0x1018f3a58>

可以发现它返回的是一个 Android 对象。 这个 Android 对象实际上属于 airtest.core.android 这个包,继承自 airtest.core.device.Device 这个类,与之并列的还有 airtest.core.ios.ios.IOSairtest.core.linux.linux.Linuxairtest.core.win.win.Windows 等。这些都有一些针对 Device 操作的 API,下面我们以 airtest.core.android.android.Android 为例来总结一下。

  • 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:调整屏幕适配分辨率

了解了上面的方法之后,我们可以用一个实例来感受下它们的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from airtest.core.android import Android
from airtest.core.api import *
import logging

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

device: Android = init_device('Android')
is_locked = device.is_locked()
print(f'is_locked: {is_locked}')

if is_locked:
device.unlock()

device.wake()

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

uuid = device.uuid
print(f'uuid {uuid}')

display_info = device.get_display_info()
print(f'display info {display_info}')

resolution = device.get_render_resolution()
print(f'resolution {resolution}')

ip_address = device.get_ip_address()
print(f'ip address {ip_address}')

top_activity = device.get_top_activity()
print(f'top activity {top_activity}')

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

这里我们调用了设备的一些操作方法,获取了一些基本状态,运行结果如下:

1
2
3
4
5
6
7
8
is_locked: False
app list ['com.kimcy929.screenrecorder', 'com.android.providers.telephony', 'io.appium.settings', 'com.android.providers.calendar', 'com.android.providers.media', 'com.goldze.mvvmhabit', 'com.android.wallpapercropper', 'com.android.documentsui', 'com.android.galaxy4', 'com.android.externalstorage', 'com.android.htmlviewer', 'com.android.quicksearchbox', 'com.android.mms.service', 'com.android.providers.downloads', 'mark.qrcode', ..., 'com.google.android.play.games', 'io.kkzs', 'tv.danmaku.bili', 'com.android.captiveportallogin']
uuid emulator-5554
display info {'id': 0, 'width': 1080, 'height': 1920, 'xdpi': 320.0, 'ydpi': 320.0, 'size': 6.88, 'density': 2.0, 'fps': 60.0, 'secure': True, 'rotation': 0, 'orientation': 0.0, 'physical_width': 1080, 'physical_height': 1920}
resolution (0.0, 0.0, 1080.0, 1920.0)
ip address 10.0.2.15
top activity ('com.microsoft.launcher.dev', 'com.microsoft.launcher.Launcher', '16040')
is keyboard shown False

连接 device

连接 device 需要传入设备的 uri,格式类似 android://adbhost:adbport/serialno?param=value&param2=value2,使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
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
"""

示例如下:

1
2
3
4
5
6
from airtest.core.android import Android
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
device: Android = connect_device(uri)
print(device)

运行结果如下:

1
<airtest.core.android.android.Android object at 0x110246940>

其实返回结果和 init_device 是一样的,最后 connect_device 方法就是调用了 init_device 方法。

获取当前 device

就是直接调用 device 方法,定义如下:

1
2
3
4
5
6
7
def device():
"""
Return the current active device.

:return: current device instance
"""
return G.DEVICE

获取所有 device

在 airtest 中有一个全局变量 G,获取所有 device 的方法如下:

1
2
3
4
5
6
7
from airtest.core.android import Android
from airtest.core.api import *

print(G.DEVICE_LIST)
uri = 'Android://127.0.0.1:5037/emulator-5554'
device: Android = connect_device(uri)
print(G.DEVICE_LIST)

运行结果如下:

1
2
[]
[<airtest.core.android.android.Android object at 0x10ba03978>]

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

切换 device

我们可以使用 set_current 方法切换当前连接的 device,传入的是 index,定义如下:

1
2
3
4
5
6
7
8
9
def set_current(idx):
"""
Set current active device.

:param idx: uuid or index of initialized device instance
:raise IndexError: raised when device idx is not found
:return: None
:platforms: Android, iOS, Windows
"""

这个方法没有返回值,调用 set_current 方法切换 device 之后,再调用 device 方法就可以获取当前 device 对象了。

执行命令行

可以使用 shell 方法传入 cmd 来执行命令行,定义如下:

1
2
3
4
5
6
7
8
9
10
@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)

直接调用 adb 命令就好了,例如获取内存信息就可以使用如下命令:

1
2
3
4
5
6
7
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)

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

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
MemTotal:        3627908 kB
MemFree: 2655560 kB
MemAvailable: 2725928 kB
Buffers: 3496 kB
Cached: 147472 kB
SwapCached: 0 kB
Active: 744592 kB
Inactive: 126332 kB
Active(anon): 723292 kB
Inactive(anon): 16344 kB
Active(file): 21300 kB
Inactive(file): 109988 kB
Unevictable: 0 kB
Mlocked: 0 kB
HighTotal: 2760648 kB
HighFree: 2073440 kB
LowTotal: 867260 kB
LowFree: 582120 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 0 kB
Writeback: 0 kB
AnonPages: 720100 kB
Mapped: 127720 kB
Shmem: 19428 kB
Slab: 76196 kB
SReclaimable: 7392 kB
SUnreclaim: 68804 kB
KernelStack: 7896 kB
PageTables: 8544 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 1813952 kB
Committed_AS: 21521776 kB
VmallocTotal: 122880 kB
VmallocUsed: 38876 kB
VmallocChunk: 15068 kB
DirectMap4k: 16376 kB
DirectMap4M: 892928 kB

启动和停止 App

启动和停止 App 直接传入包名即可,其实它们就是调用的 device 的 start_app 和 stop_app 方法,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@logwrap
def start_app(package, activity=None):
"""
Start the target application on device

:param package: name of the package to be started, e.g. "com.netease.my"
:param activity: the activity to start, default is None which means the main activity
:return: None
:platforms: Android, iOS
"""
G.DEVICE.start_app(package, activity)

@logwrap
def stop_app(package):
"""
Stop the target application on device

:param package: name of the package to stop, see also `start_app`
:return: None
:platforms: Android, iOS
"""
G.DEVICE.stop_app(package)

用法示例如下:

1
2
3
4
5
6
7
8
9
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(10)
stop_app(package)

这里我指定了微信的包名,然后调用 start_app 启动了微信,然后等待了 10 秒,然后调用了 stop_app 停止了微信。

安装和卸载

安装和卸载也是一样,也是调用了 device 的 install 和 uninstall 方法,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@logwrap
def install(filepath, **kwargs):
"""
Install application on device

:param filepath: the path to file to be installed on target device
:param kwargs: platform specific `kwargs`, please refer to corresponding docs
:return: None
:platforms: Android
"""
return G.DEVICE.install_app(filepath, **kwargs)

@logwrap
def uninstall(package):
"""
Uninstall application on device

:param package: name of the package, see also `start_app`
:return: None
:platforms: Android
"""
return G.DEVICE.uninstall_app(package)

截图

截图使用 snapshot 即可完成,可以设定存储的文件名称,图片质量等。 定义如下:

1
2
3
4
5
6
7
8
9
10
11
def snapshot(filename=None, msg="", quality=ST.SNAPSHOT_QUALITY):
"""
Take the screenshot of the target device and save it to the file.

:param filename: name of the file where to save the screenshot. If the relative path is provided, the default
location is ``ST.LOG_DIR``
:param msg: short description for screenshot, it will be recorded in the report
:param quality: The image quality, integer in range [1, 99]
:return: absolute path of the screenshot
:platforms: Android, iOS, Windows
"""

示例如下:

1
2
3
4
5
6
7
8
9
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)

运行之后在当前目录会生成一个 weixin.png 的图片,如图所示: weixin

唤醒和首页

唤醒和回到首页分别也是调用了 device 的 wake 和 home 方法,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@logwrap
def wake():
"""
Wake up and unlock the target device

:return: None
:platforms: Android

.. 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
"""
G.DEVICE.home()

直接调用即可。

点击屏幕

点击屏幕是 touch 方法,可以传入一张图或者绝对位置,同时可以指定点击次数,定义如下:

1
2
3
4
5
6
7
8
9
10
11
@logwrap
def touch(v, times=1, **kwargs):
"""
Perform the touch action on the device screen

:param v: target to touch, either a Template instance or absolute coordinates (x, y)
:param times: how many touches to be performed
:param kwargs: platform specific `kwargs`, please refer to corresponding docs
:return: finial position to be clicked
:platforms: Android, Windows, iOS
"""

例如我现在的手机屏幕是这样子: image-20200723104955681 这里我截图下来一张图片,如图所示: tpl 然后我们把这个图片声明成一个 Template 传入,示例如下:

1
2
3
4
5
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
touch(Template('tpl.png'))

启动之后它就会识别出这张图片的位置,然后点击。 或者我们可以指定点击的绝对位置,示例如下:

1
2
3
4
5
6
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
touch((400, 600), times=2)

另外上述的 touch 方法还可以完全等同于 click 方法。 如果要双击的话,还可以使用调用 double_click 方法,传入参数也可以是 Template 或者绝对位置。

滑动

滑动可以使用 swipe 方法,可以传入起始和终止位置,两个位置都可以传入绝对位置或者 Template,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@logwrap
def swipe(v1, v2=None, vector=None, **kwargs):
"""
Perform the swipe action on the device screen.

There are two ways of assigning the parameters
* ``swipe(v1, v2=Template(...))`` # swipe from v1 to v2
* ``swipe(v1, vector=(x, y))`` # swipe starts at v1 and moves along the vector.

:param v1: the start point of swipe,
either a Template instance or absolute coordinates (x, y)
:param v2: the end point of swipe,
either a Template instance or absolute coordinates (x, y)
:param vector: a vector coordinates of swipe action, either absolute coordinates (x, y) or percentage of
screen e.g.(0.5, 0.5)
:param **kwargs: platform specific `kwargs`, please refer to corresponding docs
:raise Exception: general exception when not enough parameters to perform swap action have been provided
:return: Origin position and target position
:platforms: Android, Windows, iOS
"""

比如这里我们可以定义手指向右滑动:

1
2
3
4
5
6
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
swipe((200, 300), (900, 300))

放大缩小

放大缩小是使用的 pinch 方法,可以指定放大还是缩小,同时还可以指定中心位置点和放大缩小的比率。 定义如下:

1
2
3
4
5
6
7
8
9
10
11
@logwrap
def pinch(in_or_out='in', center=None, percent=0.5):
"""
Perform the pinch action on the device screen

:param in_or_out: pinch in or pinch out, enum in ["in", "out"]
:param center: center of pinch action, default as None which is the center of the screen
:param percent: percentage of the screen of pinch action, default is 0.5
:return: None
:platforms: Android
"""

示例如下:

1
2
3
4
5
6
from airtest.core.api import *

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
pinch(in_or_out='out', center=(300, 300), percent=0.4)

键盘事件

可以使用 keyevent 来输入某个键,例如 home、back 等等,keyevent 的定义如下:

1
2
3
4
5
6
7
8
9
def keyevent(keyname, **kwargs):
"""
Perform key event on the device

:param keyname: platform specific key name
:param **kwargs: platform specific `kwargs`, please refer to corresponding docs
:return: None
:platforms: Android, Windows, iOS
"""

示例如下:

1
keyevent("HOME")

这样就表示按了 HOME 键。

输入内容

输入内容需要使用 text 方法,当然前提是这个 widget 需要是 active 状态,text 的定义如下:

1
2
3
4
5
6
7
8
9
10
@logwrap
def text(text, enter=True, **kwargs):
"""
Input text on the target device. Text input widget must be active first.

:param text: text to input, unicode is supported
:param enter: input `Enter` keyevent after text input, default is True
:return: None
:platforms: Android, Windows, iOS
"""

等待和判断

可以使用 wait 方法等待某个内容加载出来,需要传入的是 Template,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@logwrap
def wait(v, timeout=None, interval=0.5, intervalfunc=None):
"""
Wait to match the Template on the device screen

:param v: target object to wait for, Template instance
:param timeout: time interval to wait for the match, default is None which is ``ST.FIND_TIMEOUT``
:param interval: time interval in seconds to attempt to find a match
:param intervalfunc: called after each unsuccessful attempt to find the corresponding match
:raise TargetNotFoundError: raised if target is not found after the time limit expired
:return: coordinates of the matched target
:platforms: Android, Windows, iOS
"""

同时也使用 exists 方法判断某个内容是否存在,定义如下:

1
2
3
4
5
6
7
8
9
@logwrap
def exists(v):
"""
Check whether given target exists on device screen

:param v: target to be checked
:return: False if target is not found, otherwise returns the coordinates of the target
:platforms: Android, Windows, iOS
"""

断言

另外 Airtest 还提供了几个断言语句来判断结果是否存在或者相同,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@logwrap
def assert_exists(v, msg=""):
"""
Assert target exists on device screen

:param v: target to be checked
:param msg: short description of assertion, it will be recorded in the report
:raise AssertionError: if assertion fails
:return: coordinates of the target
:platforms: Android, Windows, iOS
"""
try:
pos = loop_find(v, timeout=ST.FIND_TIMEOUT, threshold=ST.THRESHOLD_STRICT)
return pos
except TargetNotFoundError:
raise AssertionError("%s does not exist in screen, message: %s" % (v, msg))

@logwrap
def assert_not_exists(v, msg=""):
"""
Assert target does not exist on device screen

:param v: target to be checked
:param msg: short description of assertion, it will be recorded in the report
:raise AssertionError: if assertion fails
:return: None.
:platforms: Android, Windows, iOS
"""
try:
pos = loop_find(v, timeout=ST.FIND_TIMEOUT_TMP)
raise AssertionError("%s exists unexpectedly at pos: %s, message: %s" % (v, pos, msg))
except TargetNotFoundError:
pass

@logwrap
def assert_equal(first, second, msg=""):
"""
Assert two values are equal

:param first: first value
:param second: second value
:param msg: short description of assertion, it will be recorded in the report
:raise AssertionError: if assertion fails
:return: None
:platforms: Android, Windows, iOS
"""
if first != second:
raise AssertionError("%s and %s are not equal, message: %s" % (first, second, msg))

@logwrap
def assert_not_equal(first, second, msg=""):
"""
Assert two values are not equal

:param first: first value
:param second: second value
:param msg: short description of assertion, it will be recorded in the report
:raise AssertionError: if assertion
:return: None
:platforms: Android, Windows, iOS
"""
if first == second:
raise AssertionError("%s and %s are equal, message: %s" % (first, second, msg))

这几个断言比较常用的就是 assert_exists 和 assert_not_exists 判断某个目标是否存在于屏幕上,同时还可以传入 msg,它可以被记录到 report 里面。 以上就是 Airtest 的 API 的用法,它提供了一些便捷的方法封装,同时还对接了图像识别等技术。 但 Airtest 也有一定的局限性,比如不能根据 DOM 树来选择对应的节点,依靠图像识别也有一定的不精确之处,所以还需要另外一个库 —— Poco。

Poco

利用 Poco 我们可以支持 DOM 选择,例如编写 XPath 等来定位某一个节点。 首先需要安装 Poco,使用 pip3 即可:

1
pip3 install pocoui

安装好了之后我们便可以使用它来选择一些节点了,示例如下:

1
2
3
4
5
6
7
8
9
from airtest.core.api import *
from poco.drivers.android.uiautomation import AndroidUiautomationPoco

poco = AndroidUiautomationPoco()

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
poco(text='Weather').click()

比如这里我们就声明了 AndroidUiautomationPoco 这个 Poco 对象,然后调用了 poco 传入一些选择器,选中之后执行 click 方法。 这个选择器非常强大,可以传入 name 和各个属性值,具体的使用方法见:https://poco.readthedocs.io/en/latest/source/poco.pocofw.html

1
2
3
4
5
6
7
8
9
10
11
from airtest.core.api import *
from poco.drivers.android.uiautomation import AndroidUiautomationPoco
from poco.proxy import UIObjectProxy

poco = AndroidUiautomationPoco()

uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
object: UIObjectProxy = poco("com.microsoft.launcher.dev:id/workspace")
print(object)

poco 返回的是 UIObjectProxy 对象,它提供了其他的操作 API,例如选取子节点,兄弟节点,父节点等等,同时可以调用各个操作方法,如 click、pinch、scroll 等等。 具体的操作文档可以参见:https://poco.readthedocs.io/en/latest/source/poco.proxy.html 下面简单总结:

  • attr:获取节点属性
  • child:获取子节点
  • children:获取所有子节点
  • click:点击
  • double_click:双击
  • drag_to:将某个节点拖拽到另一个节点
  • exists:某个节点是否存在
  • focus:获得焦点
  • get_bounds:获取边界
  • get_name:获取节点名
  • get_position:获取节点位置
  • get_size:获取节点大小
  • get_text:获取文本内容
  • long_click:长按
  • offspring:选出包含直接子代的后代
  • parent:父节点
  • pinch:缩放
  • scroll:滑动
  • set_text:设置文字
  • sibling:兄弟节点
  • swipe:滑动
  • wait:等待
  • wait_for_appearance:等待某个节点出现
  • wait_for_disappearance:等待某个节点消失

以上的这些方法混用的话就可以执行各种节点的选择和相应的操作。