环境搭建之第一个测试程序

什么是Minium?

minium是为小程序专门开发的自动化框架,使用minium可以进行小程序UI自动化测试。 当然,它的能力不仅仅局限于UI自动化, 比如:

  • 使用minium来进行函数的mock
  • 可以直接跳转到小程序某个页面
  • 设置页面数据, 做针对性的全面测试

这些能力是其他的一些工具所不具备的,不仅如此,它还有许多其他特性,也是很吸引人的:

  • 支持一套脚本,iOS &Android& 模拟器,三端运行
  • 提供丰富的页面跳转方式,看不到也能去得到
  • 可以获取和设置小程序页面数据,让测试不止点点点
  • 可以直接触发小程序元素绑定事件
  • 支持往AppSerive注入代码片段执行
  • 可以调用部分wx对象上的接口
  • 支持Mock wx对象上的接口
  • 支持Hook wx对象上的接口
  • 通过suite方式管理用例,config管理运行设备
  • ...

环境搭建

准备工作

自动安装

pip3 install minium或者
​pip3 install https://minitest.weixin.qq.com/minium/Python/dist/minium-latest.zip​

手动安装

下载 minium安装包, 解压后进入文件夹, 运行

1
python3 setup.py install

设置微信开发者工具

找开发要源代码

这里我们以官方示例小程序项目作为演示,使用git直接clone

1
git clone https://github.com/wechat-miniprogram/miniprogram-demo.git

下载到本地之后,先cdminiprogram-demo中,然后cnpm i。接着再cdminiprogram中,再次cnpm i。(npm 真不好使)

安装、依赖、编译成功,就可以看到如下小程序:

环境确认

minium安装完成后,可执行以下命令查看版本:

输入minitest -v

出现如下表示安装成功

开发者工具自动化能力检查

1
path/to/cli" auto --project "path/to/project" --auto-port 9420

路径说明:

  • path/to/project: 指代填写存放小程序源码的目录地址,文件夹中需要包含有project.config.json文件
  • path/to/cli: 指代开发者工具cli命令路径。macOS: <安装路径>/Contents/MacOS/cliWindows: <安装路径>/cli.bat
  • 有类似以下log并且开发者工具上有以下提示的则通过,否则根据提示和开发者工具文档调试解决

举个栗子:

以我win10系统为例,输入如下命令:
​​​"E:\Program Files (x86)\Tencent\微信web开发者工具\cli.bat" auto --project "D:\pyworkspace\miniprogram-demo" --auto-port 9420​

看到如下显示,证明安装成功且小程序也会被启动!

编写测试脚本

项目结构

简单创建一个python项目即可,如下所示:

添加配置文件

在项目目录添加suite.json,示例如下:

1
2
3
4
5
6
7
8
9
10
{
"pkg_list": [
{
"case_list": [
"test_*"
],
"pkg": "test_case.*_test"
}
]
}

在项目目录添加config.json,这里替换上你自己本地环境的对应路径。示例如下:

1
2
3
4
5
{
"project_path": "D:\\pyworkspace\\miniprogram-demo",
"dev_tool_path": "E:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat",
"debug_mode": "warn"
}

编写测试代码

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# -*- coding: utf-8 -*-
"""
@Time : 2022/12/30 15:48
"""
import minium


class ComponentTest(minium.MiniTest):
def test_ui_op(self):
self.page.get_element("view", inner_text="视图容器").click()
self.page.get_element("navigator", inner_text="swiper").click()
self.page.get_elements("switch")[0].click()
self.page.get_elements("switch")[1].click()
print("执行测试结束!")

执行测试脚本

运行结果如下:

命令行形式

1
minitest -m test_case.first_test -c config.json -g -s suite.json

运行结果如下:

效果

生成测试报告

输入如下命令,可生成一份美丽的测试报告

1
python -m http.server 12345 -d outputs

打开浏览器,访问http://localhost:12345即可查看报告。

项目配置及测试套件使用说明

搞定配置项

配置文件部分

示例如下:

1
2
3
4
5
{
"project_path": "D:\\pyworkspace\\miniprogram-demo",
"dev_tool_path": "E:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat",
"debug_mode": "warn"
}

这里很多参数没写,即走默认的配置项,关于详细的测试配置说明,请参看 官方文档说明

命令行工具

测试用例既可以用unittest的方式执行,也可以用minitest来加载用例执行,相关的参数说明如下:

minitest 命令

  • -h, --help: 使用帮助。
  • -v, --version: 查看 minium 的版本。
  • -p PATH/--path PATH: 用例所在的文件夹,默认当前路径。
  • -m MODULE_PATH, --module MODULE_PATH: 用例的包名或者文件名
  • --case CASE_NAME: test_开头的用例名
  • -s SUITE, --suite SUITE:就是suite.json文件,文件的格式如下:
1
2
3
4
5
6
7
8
9
10
{
"pkg_list": [
{
"case_list": [
"test_*"
],
"pkg": "test_case.*_test"
}
]
}

说明:

  • pkg_list字段说明要执行用例的内容和顺序,是一个数组,每个数组元素是一个匹配规则,会根据pkg去匹配包名,找到测试类,然后
  • 根据case_list里面的规则去查找测试类的测试用例。可以根据需要编写匹配的粒度。注意匹配规则不是正则表达式,而是通配符。

入门栗子

目录结构

编写第一个case

1
2
3
4
5
6
7
8
9
10
11
12
# -*- coding: utf-8 -*-
"""
# @Time : 2023/01/25 12:49
"""
import minium


class SysInfoTest(minium.MiniTest):
def test_sysinfo(self):
sys_info = self.mini.get_system_info()
print(sys_info)
self.assertIn("SDKVersion", sys_info)

运行单个case

执行如下命令:

1
minitest -m test_case.sysinfo_test -c config.json -g

重点:

测试用例的命名,一定要casename_test,否则不好使,不信你可以写成test_casename,命令行执行体验报错的感觉!

运行结果如下:

批量执行测试

执行如下命令:

1
minitest -s suite.json -c config.json -g

运行结果如下:

测试套件的意义在于可以批量执行用例,也是我们做自动化测试首选的方式。

部分参数说明:

  • -c CONFIG, --config CONFIG:配置文件名,配置项目参考 配置文件
  • -g, --generate: 生成网页测试报告
  • --module_search_path [SYS_PATH_LIST [SYS_PATH_LIST ...]] : 添加 module 的搜索路径
  • -a, --accounts: 查看开发者工具当前登录的多账号, 需要通过 9420 端口,以自动化模式打开开发者工具
  • --mode RUN_MODE: 选择以parallel(并行, 每个账号从队列中取一个pkg运行, 完成后取下一个)或者fork(复刻, 每个帐号都跑全部的pkg)的方式运行用例
  • --task-limit-time: 任务超时时间,如果到期还没跑完测试,直接终止测试进程. 单位: s

更多命令行参数请参考 命令行工具

生成测试报告

本地报告

执行如下命令:
​​​python -m http.server 12345 -d outputs​

打开浏览器,访问http://localhost:12345即可查看报告。

利用nginx的配置报告

1
2
3
4
5
6
7
8
9
server {
listen 80;
server_name your.domain.com;

location / {
alias /path/to/dir/of/report;
index index.html;
}
}

元素定位详解

元素定位

元素定位,应该是很多UI自动化测试入门学习必会的技能了,下面我将为大家举例演示元素定位的几种方法。

CSS选择器

Minium 可以通过 WXSS 选择器定位元素,如下图所示:

如果有[CSS选择器]基础会上手更快 ,如没有可参考

示例:

CSS方式定位

示例代码如下:

1
2
3
4
5
6
7
8
# class定位
self.page.get_element(".kind-list-item-hd").click()
# id定位
self.page.get_element("#view").click()
# 属性定位(逻辑运算定位)
self.page.get_element("[id='view'][class='kind-list-item-hd']").click()
# 简单选择器格式 tageName + #id + .className
self.page.get_element("view#view.kind-list-item-hd").click()

XPATH方式定位

看到这里,有的同学可能会条件反射性的想到,右键选择 Copy,点击 Copy XpathCopy full Xpath

没错,是支持的!

使用xpath语法定位,示例代码如下:

1
2
3
4
5
6
7
8
9
10
# 绝对定位
self.page.get_element("/page/scroll-view/view/view/view[2]/view/view[1]/view[1]").click()
# 相对定位
self.page.get_element("//view[@id='view']").click()
# 使用属性定位
self.page.get_element("//view[@id='view' and @class='kind-list-item-hd']").click()
# 使用部分属性定位
self.page.get_element("//view[contains(@id,'vi')]").click()
# 使用文本定位
self.page.get_element("//view[text()='视图容器']").click()

个人感觉是完美兼容SeleniumCSS 、XPATH定位方式的,参考学习CSS定位入门、XPATH定位入门这两篇。

selector方式定位

推荐使用id/class/标签+属性inner_text/text_contains/value为增强用法,实现本质还是通过selector获取到元素标签后再通过inner_text/text_contains/value筛选元素。
示例代码如下:

1
2
3
# 适合没有属性元素定位
self.page.get_element("view", inner_text="视图容器").click()
self.page.get_element("navigator", inner_text="swiper").click()

跨自定义组件元素定位方式

怎样识别自定义组件

  • wxml文件或微信开发者工具的wxml pannel,标签名字不在小程序官方组件列表中的都是 自定义组件
  • 看微信开发者工具的wxml pannel, 标签下面有#shadow-root的, 则为 自定义组件 。例如小程序页面wxmlmytest 、test2、 test22

定位 **test2** 标签下的 text 的元素

示例代码如下:

1
2
3
4
5
6
7
8
# 没有元素属性,可以文本定位一试
self.page.get_element("text", inner_text="this is test2").click()
# 绝对对位(dom结构改变就完蛋,不推荐)
self.page.get_element("/page/mytest//test2//view/view/text")
# 使用跨自定义组件的后代选择器
self.page.get_element("mytest>>>test2>>>text") # 自定义组件 mytest组件节点下的test2组件节点下text标签
# 逐层查找定位
self.page.get_element("mytest").get_element("test2").get_element("#test2").get_element("text")

定位示例详解

这里我贴出源代码给的注释部分,个人觉得这个注释写的真好,一看就懂,如下图所示:

总结

元素定位小结:

  • 不建议使用基础标签view、text方式定位元素,有时会因为dom加载不出来找不到元素,源码中参数max_timeout=0,有需要可自己指定超时时间。
  • 小程序发版频繁不建议使用绝对定位,使用有一定标识性或属性结合定位,可考虑选择器定位
  • 若元素没有属性,则可考虑XPath,或标签+文本定位
  • 自定义组件定位,可以考虑跨自定义组件的后代选择器或逐层定位元素方法定位

API详解(上)

常用API使用详解

get_system_info()

获取系统信息

shutdown()

测试结束时调用, 停止 微信开发者IDE 以及 minium, 并回收资源。一般供测试框架调用

screen_shot()

截图

ide上仅能截取到wxml页面的内容,Modal/Actionsheet/授权弹窗等无法截取

Parameters:

名称类型默认值说明
save_pathstrNot None截图保存路径
formatstrraw截图数据返回格式,raw 或者 pillow

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
def test_screen_shot(self):
"""
截图操作
:return:
"""
output_path = os.path.join(os.path.dirname(__file__), "outputs/test_screen_shot.png")
print(output_path)
if not os.path.isdir(os.path.dirname(output_path)):
os.mkdir(os.path.dirname(output_path))
if os.path.isfile(output_path):
os.remove(output_path)
ret = self.app.screen_shot(output_path) # 截图并存到`output_path`文件夹中
self.assertTrue(os.path.isfile(output_path))

evaluate()

向 app Service 层注入代码并执行

真机调试2.0下, 注入的代码只支持es5的语法

Parameters:

名称类型默认值说明
app_functionstrNot None代码字符串
argslistNot None参数
syncboolFalse是否同步执行

Returns:

  • sync == True: dict(result={"result": 函数返回值})
  • sync == False: str(消息ID)。配合get_async_response用获取返回值

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import minium

# sync == True
@minium.ddt_class
class TestApp(minium.MiniTest):
@minium.ddt_case([], ["1", "2"])
def test_evaluate_sync(self, args):
result = self.app.evaluate(
"function(){args=arguments;return 'test evaluate: '.concat(Array.from(args));}", args, sync=True
)
self.assertEqual(
result.get("result", {}).get("result"), "test evaluate: {}".format(",".join(args))
)

get_async_response()

获取evaluate方法异步调用的结果

Parameters:

名称类型默认值说明
msg_idstrNot Noneevaluate返回的消息ID
timeoutintNone等待超时时间,None: 立刻返回

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import minium

# sync == False
@minium.ddt_class
class TestApp(minium.MiniTest):
@minium.ddt_case([], ["1", "2"])
def test_evaluate_async(self, args):
msg_id = self.app.evaluate(
"function(){args=arguments;return 'test evaluate: '.concat(Array.from(args));}", args, sync=False
)
# 你可以做一些其他操作后, 再通过get_async_response方法获取前面注入代码的运行结果
result = self.app.get_async_response(msg_id, 5)
self.assertEqual(
result.get("result", {}).get("result"), "test evaluate: {}".format(",".join(args))
)

get_all_pages_path()

获取所有已配置的页面路径

Returns:

  • list

示例代码如下:

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def test_get_all_pages_path(self):
"""
获取所有已配置的页面路径
:return:list
"""
all_pages_path = self.app.get_all_pages_path()
self.assertListEqual(
['page/component/index', 'page/API/index', 'page/cloud/index', 'page/extend/index', 'page/animation/index',
'packageComponent/pages/view/view/view', 'packageComponent/pages/view/scroll-view/scroll-view',
'packageComponent/pages/view/swiper/swiper', 'packageComponent/pages/view/movable-view/movable-view',
'packageComponent/pages/view/cover-view/cover-view', 'packageComponent/pages/content/text/text',
'packageComponent/pages/content/icon/icon', 'packageComponent/pages/content/progress/progress',
'packageComponent/pages/content/rich-text/rich-text', 'packageComponent/pages/form/button/button',
'packageComponent/pages/form/checkbox/checkbox', 'packageComponent/pages/form/form/form',
'packageComponent/pages/form/input/input', 'packageComponent/pages/form/label/label',
'packageComponent/pages/form/picker/picker', 'packageComponent/pages/form/picker-view/picker-view',
'packageComponent/pages/form/radio/radio', 'packageComponent/pages/form/slider/slider',
'packageComponent/pages/form/switch/switch', 'packageComponent/pages/form/textarea/textarea',
'packageComponent/pages/form/editor/editor', 'packageComponent/pages/nav/navigator/navigator',
'packageComponent/pages/nav/navigator/navigate', 'packageComponent/pages/nav/navigator/redirect',
'packageComponent/pages/media/image/image', 'packageComponent/pages/media/video/video',
'packageComponent/pages/media/camera/camera', 'packageComponent/pages/media/live-pusher/live-pusher',
'packageComponent/pages/media/live-player/live-player', 'packageComponent/pages/map/map/map',
'packageComponent/pages/canvas/canvas-2d/canvas-2d', 'packageComponent/pages/canvas/webgl/webgl',
'packageComponent/pages/open/ad/ad', 'packageComponent/pages/open/open-data/open-data',
'packageComponent/pages/open/web-view/web-view',
'packageComponent/pages/obstacle-free/aria-component/aria-component',
'packageComponent/pages/doc-web-view/doc-web-view', 'packageAPI/pages/api/login/login',
'packageAPI/pages/api/get-user-info/get-user-info', 'packageAPI/pages/api/request-payment/request-payment',
'packageAPI/pages/api/jump/jump', 'packageAPI/pages/api/share/share',
'packageAPI/pages/api/share-button/share-button', 'packageAPI/pages/api/custom-message/custom-message',
'packageAPI/pages/api/subscribe-message/subscribe-message',
'packageAPI/pages/api/choose-address/choose-address',
'packageAPI/pages/api/choose-invoice-title/choose-invoice-title',
'packageAPI/pages/api/soter-authentication/soter-authentication', 'packageAPI/pages/api/setting/setting',
'packageAPI/pages/ar/visionkit-basic/visionkit-basic',
'packageAPI/pages/ar/visionkit-basic-v2/visionkit-basic-v2', 'packageAPI/pages/ar/plane-ar/plane-ar',
'packageAPI/pages/ar/plane-ar-v2/plane-ar-v2', 'packageAPI/pages/ar/plane-ar-3dof/plane-ar-3dof',
'packageAPI/pages/ar/2dmarker-ar/2dmarker-ar', 'packageAPI/pages/ar/3dmarker-ar/3dmarker-ar',
'packageAPI/pages/ar/osd-ar/osd-ar', 'packageAPI/pages/ar/face-detect/face-detect',
'packageAPI/pages/ar/body-detect/body-detect', 'packageAPI/pages/ar/hand-detect/hand-detect',
'packageAPI/pages/ar/ocr-detect/ocr-detect', 'packageAPI/pages/ar/photo-ocr-detect/photo-ocr-detect',
'packageAPI/pages/ar/photo-hand-detect/photo-hand-detect',
'packageAPI/pages/ar/photo-body-detect/photo-body-detect',
'packageAPI/pages/ar/photo-face-detect/photo-face-detect',
'packageAPI/pages/page/set-navigation-bar-title/set-navigation-bar-title',
'packageAPI/pages/page/navigation-bar-loading/navigation-bar-loading',
'packageAPI/pages/page/navigator/navigator', 'packageAPI/pages/page/pull-down-refresh/pull-down-refresh',
'packageAPI/pages/page/animation/animation', 'packageAPI/pages/page/action-sheet/action-sheet',
'packageAPI/pages/page/modal/modal', 'packageAPI/pages/page/toast/toast',
'packageAPI/pages/page/canvas/canvas', 'packageAPI/pages/page/get-wxml-node-info/get-wxml-node-info',
'packageAPI/pages/page/page-scroll/page-scroll',
'packageAPI/pages/page/intersection-observer/intersection-observer',
'packageAPI/pages/device/clipboard-data/clipboard-data', 'packageAPI/pages/device/bluetooth/bluetooth',
'packageAPI/pages/device/bluetooth/slave/slave',
'packageAPI/pages/device/screen-brightness/screen-brightness', 'packageAPI/pages/device/vibrate/vibrate',
'packageAPI/pages/device/add-contact/add-contact', 'packageAPI/pages/device/wifi/wifi',
'packageAPI/pages/device/get-network-type/get-network-type',
'packageAPI/pages/device/on-network-status-change/on-network-status-change',
'packageAPI/pages/device/get-system-info/get-system-info',
'packageAPI/pages/device/on-compass-change/on-compass-change',
'packageAPI/pages/device/make-phone-call/make-phone-call', 'packageAPI/pages/device/scan-code/scan-code',
'packageAPI/pages/device/on-accelerometer-change/on-accelerometer-change',
'packageAPI/pages/device/capture-screen/capture-screen', 'packageAPI/pages/device/ibeacon/ibeacon',
'packageAPI/pages/device/get-battery-info/get-battery-info', 'packageAPI/pages/media/image/image',
'packageAPI/pages/media/voice/voice', 'packageAPI/pages/media/file/file',
'packageAPI/pages/media/load-font-face/load-font-face',
'packageAPI/pages/media/background-audio/background-audio', 'packageAPI/pages/media/video/video',
'packageAPI/pages/media/audio/audio', 'packageAPI/pages/media/media-container/media-container',
'packageAPI/pages/location/get-location/get-location',
'packageAPI/pages/location/open-location/open-location',
'packageAPI/pages/location/choose-location/choose-location', 'packageAPI/pages/network/request/request',
'packageAPI/pages/network/web-socket/web-socket', 'packageAPI/pages/network/upload-file/upload-file',
'packageAPI/pages/network/download-file/download-file', 'packageAPI/pages/network/mdns/mdns',
'packageAPI/pages/network/udp-socket/udp-socket', 'packageAPI/pages/storage/storage/storage',
'packageAPI/pages/storage/get-background-fetch-data/get-background-fetch-data',
'packageAPI/pages/storage/get-background-prefetch-data/get-background-prefetch-data',
'packageAPI/pages/performance/get-performance/get-performance', 'packageAPI/pages/worker/worker/worker',
'packageAPI/pages/framework/two-way-bindings/two-way-bindings', 'packageAPI/pages/framework/wxs/wxs',
'packageAPI/pages/framework/resizable/resizable', 'packageAPI/pages/framework/wxs/movable',
'packageAPI/pages/framework/wxs/sidebar', 'packageAPI/pages/framework/wxs/stick-top',
'packageAPI/pages/framework/wxs/nearby', 'packageAPI/pages/ai/mobilenet/index',
'packageAPI/pages/ai/style-trans/index', 'packageCloud/pages/user/user-authentication/user-authentication',
'packageCloud/pages/database/crud/crud', 'packageCloud/pages/database/db-permission/db-permission',
'packageCloud/pages/database/server-date/server-date',
'packageCloud/pages/storage/upload-file/upload-file',
'packageCloud/pages/storage/download-file/download-file',
'packageCloud/pages/storage/get-temp-file-url/get-temp-file-url',
'packageCloud/pages/storage/delete-file/delete-file',
'packageCloud/pages/storage/cloud-file-component/cloud-file-component',
'packageCloud/pages/scf/get-wx-context/get-wx-context', 'packageCloud/pages/scf/scf-database/scf-database',
'packageCloud/pages/scf/scf-storage/scf-storage', 'packageCloud/pages/scf/scf-openapi/scf-openapi',
'packageExtend/pages/form/cell/cell', 'packageExtend/pages/form/slideview/slideview',
'packageExtend/pages/form/form/form', 'packageExtend/pages/form/uploader/uploader',
'packageExtend/pages/base/article/article', 'packageExtend/pages/base/icons/icons',
'packageExtend/pages/base/badge/badge', 'packageExtend/pages/base/flex/flex',
'packageExtend/pages/base/footer/footer', 'packageExtend/pages/base/gallery/gallery',
'packageExtend/pages/base/grid/grid', 'packageExtend/pages/base/loadmore/loadmore',
'packageExtend/pages/base/loading/loading', 'packageExtend/pages/base/panel/panel',
'packageExtend/pages/base/preview/preview', 'packageExtend/pages/operate/dialog/dialog',
'packageExtend/pages/operate/msg/msg', 'packageExtend/pages/operate/msg/msg_success',
'packageExtend/pages/operate/msg/msg_text', 'packageExtend/pages/operate/msg/msg_text_primary',
'packageExtend/pages/operate/msg/msg_fail',
'packageExtend/pages/operate/half-screen-dialog/half-screen-dialog',
'packageExtend/pages/operate/actionsheet/actionsheet', 'packageExtend/pages/operate/toptips/toptips',
'packageExtend/pages/navigation/navigation/navigation', 'packageExtend/pages/navigation/tabbar/tabbar',
'packageExtend/pages/search/searchbar/searchbar', 'packageExtend/pages/extend/emoji/emoji',
'packageExtend/pages/extend/video-swiper/video-swiper', 'packageExtend/pages/extend/index-list/index-list',
'packageExtend/pages/extend/recycle-view/recycle-view', 'packageExtend/pages/extend/sticky/sticky',
'packageExtend/pages/extend/tabs/tabs', 'packageExtend/pages/extend/vtabs/vtabs',
'packageExtend/pages/extend/barrage/barrage', 'packageExtend/pages/extend/select-text/select-text',
'packageExtend/pages/extend/wxml-to-canvas/wxml-to-canvas',
'packageExtend/pages/adapt/telescopic/telescopic', 'packageExtend/pages/adapt/linebreak/linebreak',
'packageExtend/pages/adapt/sidenavigation/sidenavigation',
'packageExtend/pages/adapt/pagination/pagination', 'packageExtend/pages/adapt/freelayout/freelayout',
'packageExtend/pages/adapt/layeredpresentation/layeredpresentation',
'packageExtend/pages/adapt/horizontalexpansion/horizontalexpansion',
'packageSkyline/pages/worklet/animation/index', 'packageSkyline/pages/worklet/gesture/index',
'packageSkyline/pages/worklet/bottom-sheet/index', 'packageSkyline/pages/share-element/list/index',
'packageSkyline/pages/share-element/card/index', 'packageSkyline/pages/half-page/scale-page/index',
'packageSkyline/pages/half-page/half-page/index', 'packageSkyline/pages/address-book/index',
'packageSkyline/pages/half-screen/index', 'packageSkyline/pages/tabs/index',
'packageSkyline/pages/album/index', 'packageSkyline/pages/preview/index',
'packageSkyline/pages/custom-route/index', 'packageSkyline/pages/custom-route/detail',
'packageSkyline/pages/scroll-view/index', 'packageXRFrame/pages/index/index',
'packageXRFrame/pages/scene-basic/index', 'packageXRFrame/pages/scene-basic-light/index',
'packageXRFrame/pages/scene-basic-animation/index', 'packageXRFrame/pages/scene-basic-particle/index',
'packageXRFrame/pages/scene-basic-touch/index', 'packageXRFrame/pages/scene-basic-visible-layer/index',
'packageXRFrame/pages/scene-basic-shadow/index', 'packageXRFrame/pages/scene-basic-video/index',
'packageXRFrame/pages/scene-basic-render-texture/index',
'packageXRFrame/pages/scene-basic-postprocessing/index',
'packageXRFrame/pages/scene-gltf-damageHelmet/index', 'packageXRFrame/pages/scene-gltf-unlit/index',
'packageXRFrame/pages/scene-gltf-animation/index', 'packageXRFrame/pages/scene-gltf-morph/index',
'packageXRFrame/pages/scene-gltf-light-loading/index', 'packageXRFrame/pages/scene-ar-basic/index',
'packageXRFrame/pages/scene-ar-2dmarker/index', 'packageXRFrame/pages/scene-ar-osdmarker/index',
'packageXRFrame/pages/scene-ar-camera/index', 'packageXRFrame/pages/scene-ar-face/index',
'packageXRFrame/pages/scene-ar-hand/index', 'packageXRFrame/pages/scene-ar-body/index',
'packageXRFrame/pages/scene-custom-logic/index', 'packageXRFrame/pages/scene-custom-render/index',
'packageXRFrame/pages/scene-scan-render/index', 'packageXRFrame/pages/scene-scan-team/index',
'packageXRFrame/pages/scene-classic-wxball/index', 'packageXRFrame/pages/scene-classic-video/index',
'packageXRFrame/pages/scene-classic-perspect/index', 'packageXRFrame/pages/scene-classic-portal/index',
'packageXRFrame/pages/scene-classic-osd/index', 'packageXRFrame/pages/scene-classic-face/index',
'packageXRFrame/pages/scene-last-record/index'],
all_pages_path,
"test ok",
)

get_current_page()

获取当前顶层页面

Returns:

  • 页面对象

示例代码如下:

1
2
3
4
5
6
7
8
9
import minium


class AppTest(minium.MiniTest):
def test_get_current_page(self):
page = self.app.get_current_page() # 同self.app.current_page
self.assertIsNotNone(page.path)
self.assertNotEqual("", page.path)
print(page.path)

go_home()

跳转到小程序首页

示例代码如下:

1
2
3
4
5
6
7
8
9
def test_go_home(self):
"""
跳转到小程序首页
:return:
"""
self.page.get_element("view", inner_text="视图容器").click()
self.page.get_element("navigator", inner_text="view").click()
# 跳转到小程序首页
self.app.go_home()

以导航的方式跳转到指定页面

不能跳到 tabbar 页面。支持相对路径和绝对路径, 小程序中页面栈最多十层

Parameters:

名称类型默认值说明
urlstrNot None页面路径
paramsdictNone页面参数
is_wait_url_changeboolTrue是否等待新的页面跳转

个人感觉就是跳转到指定页面,也可以理解为导航栏上的超链接带路径那种的

示例代码如下:

1
2
3
4
5
6
7
def test_navigate_to(self):
"""
跳转到指定页面
:return:
"""
pass_page = self.app.get_current_page()
self.app.navigate_to("/packageComponent/pages/view/view/view")

关闭当前页面,返回上一页面

示例代码如下:

1
2
3
4
5
6
7
8
9
10
def test_navigate_to(self):
"""
返回上一页
:return:
"""
pass_page = self.app.get_current_page()
# 跳转到指定页面
self.app.navigate_to("/packageComponent/pages/view/view/view")
# 返回上一页
self.app.navigate_back()

redirect_to()

关闭当前页面,重定向到应用内的某个页面

不允许跳转到 tabbar 页面

Parameters:

名称类型默认值说明
urlstrNot None页面路径
paramsdictNone页面参数
is_wait_url_changeboolTrue是否等待新的页面跳转

个人感觉同navigate_to()用法很像,各种细节感兴趣的同学可以自行尝试。

示例代码如下:

1
2
3
4
5
6
7
def redirect_to(self):
"""
重定向到指定页面
:return:
"""
# 重定向到指定页面
self.app.redirect_to("/packageComponent/pages/view/view/view")

relaunch()

关闭所有页面,打开到应用内的某个页面

Parameters:

名称类型默认值说明
urlstrNot None页面路径

示例代码如下:

1
2
3
4
5
6
7
8
9
10
def test_relaunch(self):
"""
关闭所有页面,打开到应用内的某个页面
:return:
"""
pass_page = self.app.get_current_page()
# 重定向到指定页面
self.app.redirect_to("/packageComponent/pages/view/view/view")
# 关闭所有页面,打开到应用内的某个页面
self.app.relaunch("/packageComponent/pages/view/movable-view/movable-view")

switch_tab()

跳转到 tabBar 页面

会关闭其他所有非tabBar页面

Parameters:

名称类型默认值说明
urlstrNot None需要跳转的 tabBar 页面的路径(需在 app.json 的 tabBar 字段定义的页面),路径后不能带参数
is_clickboolFalse切换tab的时候触发一次onTabItemTap

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
def switch_tab(self):
"""
跳转到 tabBar 页面
:return:
"""
pass_page = self.app.get_current_page()
print(pass_page)
# 重定向到指定页面
self.app.redirect_to("/packageComponent/pages/view/view/view")
# 关闭所有页面,打开到应用内的某个页面
self.app.relaunch("/packageComponent/pages/view/movable-view/movable-view")
# 跳转到 tabBar 页面
self.app.switch_tab("/page/API/index")

什么是tabbar页面?

举个栗子,比如我们小程序的底部有图标加文字的几个按钮,每个按钮对应一个页面,而整个小程序中有很多页面,小程序底部图标加文字对应的几个页面是tabbar页面,这个在app.json中有设置。
eg:在app.json中设置对应的tabbar页面

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
"tabBar": {
"color": "#333",
"selectedColor": "#d43c33",
"backgroundColor": "#fff",
"position": "bottom",
"list": [
{
"pagePath": "pages/index/index",
"text": "主页",
"iconPath": "/static/images/tabs/tab-home.png",
"selectedIconPath": "/static/images/tabs/tab-home-current.png"
},
{
"pagePath": "pages/video/video",
"text": "视频",
"iconPath": "/static/images/tabs/select.png",
"selectedIconPath": "/static/images/tabs/selected.png"
},
{
"pagePath": "pages/personal/personal",
"text": "个人中心",
"iconPath": "/static/images/tabs/tab-my.png",
"selectedIconPath": "/static/images/tabs/tab-my-current.png"
}
]
}

get_perf_time()

查询小程序的性能指标,跟stop_get_perf_time配对使用

Parameters:

名称类型默认值说明
entry_typeslistNot None可选项为['render', 'script', 'navigation', 'loadPackage']中的1个或多个

stop_get_perf_time()

结束查询,跟get_perf_time配对使用

示例代码如下:

1
2
3
4
5
6
7
8
9
10
def test_get_perf_time(self):
"""
查询小程序的性能指标
:return:
"""
self.app.get_perf_time(entry_types=["navigation"])
self.app.navigate_to("/packageComponent/pages/view/view/view")
self.app.redirect_to("/packageComponent/pages/view/movable-view/movable-view")
perf_data = self.app.stop_get_perf_time()
print(perf_data)

可以看到一些路径跳转的耗时,结果如下:

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
[{
'entryType': 'navigation',
'name': 'route',
'startTime': 1674803063419,
'duration': 387,
'navigationType': 'navigateTo',
'path': 'packageComponent/pages/view/view/view',
'pageId': 53,
'referrerPath': 'page/component/index',
'referrerPageId': 52,
'navigationStart': 1674803063575
}, {
'entryType': 'render',
'name': 'firstRender',
'startTime': 1674803063575,
'duration': 74,
'path': 'packageComponent/pages/view/view/view',
'pageId': 53,
'viewLayerReadyTime': 1674803063618,
'initDataSendTime': 1674803063603,
'initDataRecvTime': 1674803063619,
'viewLayerRenderStartTime': 1674803063619,
'viewLayerRenderEndTime': 1674803063646
}, {
'entryType': 'render',
'name': 'firstPaint',
'startTime': 1674803063583,
'path': 'packageComponent/pages/view/view/view',
'pageId': 53
}, {
'entryType': 'render',
'name': 'firstContentfulPaint',
'startTime': 1674803063667,
'path': 'packageComponent/pages/view/view/view',
'pageId': 53
}, {
'entryType': 'render',
'name': 'largestContentfulPaint',
'startTime': 1674803063667,
'path': 'packageComponent/pages/view/view/view',
'pageId': 53
}, {
'entryType': 'navigation',
'name': 'route',
'startTime': 1674803063419,
'duration': 387,
'navigationType': 'navigateTo',
'path': 'packageComponent/pages/view/view/view',
'pageId': 53,
'referrerPath': 'page/component/index',
'referrerPageId': 52,
'navigationStart': 1674803063575
}, {
'entryType': 'render',
'name': 'firstRender',
'startTime': 1674803063575,
'duration': 74,
'path': 'packageComponent/pages/view/view/view',
'pageId': 53,
'viewLayerReadyTime': 1674803063618,
'initDataSendTime': 1674803063603,
'initDataRecvTime': 1674803063619,
'viewLayerRenderStartTime': 1674803063619,
'viewLayerRenderEndTime': 1674803063646
}, {
'entryType': 'render',
'name': 'firstPaint',
'startTime': 1674803063583,
'path': 'packageComponent/pages/view/view/view',
'pageId': 53
}, {
'entryType': 'render',
'name': 'firstContentfulPaint',
'startTime': 1674803063667,
'path': 'packageComponent/pages/view/view/view',
'pageId': 53
}, {
'entryType': 'render',
'name': 'largestContentfulPaint',
'startTime': 1674803063667,
'path': 'packageComponent/pages/view/view/view',
'pageId': 53
}, {
'entryType': 'navigation',
'name': 'route',
'startTime': 1674803063419,
'duration': 387,
'navigationType': 'navigateTo',
'path': 'packageComponent/pages/view/view/view',
'pageId': 53,
'referrerPath': 'page/component/index',
'referrerPageId': 52,
'navigationStart': 1674803063575
}, {
'entryType': 'navigation',
'name': 'route',
'startTime': 1674803063874,
'duration': 680,
'navigationType': 'redirectTo',
'path': 'packageComponent/pages/view/movable-view/movable-view',
'pageId': 54,
'referrerPath': 'packageComponent/pages/view/view/view',
'referrerPageId': 53,
'navigationStart': 1674803064353
}, {
'entryType': 'render',
'name': 'firstRender',
'startTime': 1674803064353,
'duration': 71,
'path': 'packageComponent/pages/view/movable-view/movable-view',
'pageId': 54,
'viewLayerReadyTime': 1674803064393,
'initDataSendTime': 1674803064367,
'initDataRecvTime': 1674803064375,
'viewLayerRenderStartTime': 1674803064394,
'viewLayerRenderEndTime': 1674803064421
}, {
'entryType': 'render',
'name': 'firstPaint',
'startTime': 1674803064358,
'path': 'packageComponent/pages/view/movable-view/movable-view',
'pageId': 54
}, {
'entryType': 'render',
'name': 'firstContentfulPaint',
'startTime': 1674803064425,
'path': 'packageComponent/pages/view/movable-view/movable-view',
'pageId': 54
}, {
'entryType': 'render',
'name': 'largestContentfulPaint',
'startTime': 1674803064425,
'path': 'packageComponent/pages/view/movable-view/movable-view',
'pageId': 54
}, {
'entryType': 'navigation',
'name': 'route',
'startTime': 1674803063874,
'duration': 680,
'navigationType': 'redirectTo',
'path': 'packageComponent/pages/view/movable-view/movable-view',
'pageId': 54,
'referrerPath': 'packageComponent/pages/view/view/view',
'referrerPageId': 53,
'navigationStart': 1674803064353
}, {
'entryType': 'render',
'name': 'firstRender',
'startTime': 1674803064353,
'duration': 71,
'path': 'packageComponent/pages/view/movable-view/movable-view',
'pageId': 54,
'viewLayerReadyTime': 1674803064393,
'initDataSendTime': 1674803064367,
'initDataRecvTime': 1674803064375,
'viewLayerRenderStartTime': 1674803064394,
'viewLayerRenderEndTime': 1674803064421
}, {
'entryType': 'render',
'name': 'firstPaint',
'startTime': 1674803064358,
'path': 'packageComponent/pages/view/movable-view/movable-view',
'pageId': 54
}, {
'entryType': 'render',
'name': 'firstContentfulPaint',
'startTime': 1674803064425,
'path': 'packageComponent/pages/view/movable-view/movable-view',
'pageId': 54
}, {
'entryType': 'render',
'name': 'largestContentfulPaint',
'startTime': 1674803064425,
'path': 'packageComponent/pages/view/movable-view/movable-view',
'pageId': 54
}, {
'entryType': 'navigation',
'name': 'route',
'startTime': 1674803063874,
'duration': 680,
'navigationType': 'redirectTo',
'path': 'packageComponent/pages/view/movable-view/movable-view',
'pageId': 54,
'referrerPath': 'packageComponent/pages/view/view/view',
'referrerPageId': 53,
'navigationStart': 1674803064353
}]

wait_for_page()

等待页面跳转成功

Parameters:

名称类型默认值说明
page_pathstrNot None需要等待的页面路径, 需要绝对路径, 如/pages/index

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def test_wait_for_page(self):
"""
等待页面跳转成功
:return:布尔类型
"""
self.page.get_element("view",inner_text='表单组件').click()
el = self.app.current_page.get_element('navigator',inner_text='button')
el.tap()
current_page=self.app.get_current_page()
time.sleep(3)
print(current_page)
ret = self.app.wait_for_page("/packageComponent/pages/form/button/button")
print(ret)
self.assertTrue(ret, "wait success")
self.assertEqual(self.app.current_page.path, "/packageComponent/pages/form/button/button", "path ok")

wait_util()

指定时间内, 剩余没有完成的异步请求数 <= {cnt}个, 此时认为页面异步加载完成

Parameters:

名称类型默认值说明
cntintNot None剩余的异步请求个数
max_timeoutint10最大等待时间

示例代码如下:

1
2
3
4
5
6
7
8
9
10
def test_wait_util(self):
"""
确定指定时间内, 异步请求是否完成的
:return:布尔类型
"""
self.page.get_element("view",inner_text='表单组件').click()
el = self.app.current_page.get_element('navigator',inner_text='button')
el.tap()
ret = self.app.wait_util(0, 5) # 5s内, 页面没有任何未完成的异步请求
self.assertTrue(ret, "wait success")

API详解(下)

Page中API的使用

data

当前页面数据, 可直接赋值

1
2
3
Page({
data: {"testdata1": 1}
})

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def test_data(self):
"""
data演示数据
:return:
"""
self.app.navigate_to("/packageComponent/pages/view/view/view")
page = self.app.get_current_page()
data = page.data
self.assertDictEqual({'theme': 'light'}, data)
data["theme"] = "red"
page.data = data
page.data = {"theme1": "light1"}
page = self.app.get_current_page()
print(page.data)
self.assertDictEqual({'theme': 'red', 'theme1': 'light1'}, page.data)

element_is_exists()

在当前页面查询元素是否存在

Parameters:

名称类型默认值说明
selectorstrNot Nonecss选择器或以///开头的xpath
max_timeoutint10超时时间,单位 s
inner_textstrNone通过控件内的文字识别控件
text_containsstrNone通过控件内的文字模糊匹配控件
valuestrNone通过控件的 value 识别控件
xpathstrNone显式指定xpath

示例代码如下:

1
2
3
4
5
6
7
8
def test_element_is_exists(self):
"""
验证在当前页面元素是否存在
:return:
"""
self.app.navigate_to("/packageComponent/pages/view/view/view")
is_exists = self.page.element_is_exists("view", inner_text="B", max_timeout=5)
self.assertEqual(True, is_exists," 在当前页面元素存在")

get_element()

获取页面元素

Parameters:

名称类型默认值说明
selectorstrNot NoneCSS选择器或以///开头的 XPath
inner_textstrNone通过控件内的文字识别控件
text_containsstrNone通过控件内的文字模糊匹配控件
valuestrNone通过控件的 value 识别控件
max_timeoutint0超时时间,单位 s
xpathstrNone显式指定 XPath, 小程序基础库2.19.5后支持

PS: selector 仅支持下列语法:

  • ID选择器:#the-id
  • class选择器(可以连续指定多个):.a-class.another-class
  • 标签选择器:view
  • 子元素选择器:.the-parent > .the-child
  • 后代选择器:.the-ancestor .the-descendant
  • 跨自定义组件的后代选择器custom-element1>>>.custom-element2>>>.the-descendantcustom-element1 和 .custom-element2必须是自定义组件标签或者能获取到自定义组件的选择器
  • 多选择器的并集:#a-node, .some-other-nodes
  • xpath:可以在真机调试的wxml pannel选择节点->右键->copy->copy full xpath获取,暂不支持[text()='xxx']这类xpath条件
  • 自定义组件不支持穿透, 需要先get自定义组件, 再使用Element.get_element获取其子节点, 或使用[>>>]连接自定义组件及其后代元素, 如发现无法正常定位, 可根据这个方法 辨别自定义组件
  • 更多元素定位实例

Returns:

实例代码如下:

1
2
3
4
5
6
7
8
9
def test_get_element(self):
'''
获取页面当前元素
:return:
'''
self.app.navigate_to("/packageComponent/pages/view/view/view")
element = self.page.get_element("view",inner_text="A", max_timeout=5)
print(element.inner_text)
print(element.inner_wxml)

get_elements()

获取一组元素

PS: 支持的选择器同 get_element()

Parameters:

名称类型默认值说明
selectorstrNot Nonecss选择器或以///开头的xpath
max_timeoutint0超时时间,单位 s
inner_textstrNone通过控件内的文字识别控件, xpath暂不支持
text_containsstrNone通过控件内的文字模糊匹配控件, xpath暂不支持
valuestrNone通过控件的 value 识别控件, xpath暂不支持
indexint-1index==-1: 获取所有符合的元素, index>=0: 获取前index+1符合的元素
xpathstrNone显式指定xpath, 小程序基础库2.19.5后支持

Returns:

示例代码如下:

1
2
3
4
5
6
7
8
def test_get_elements(self):
'''
获取一组元素
:return:
'''
element = self.page._get_elements_by_css("[class='kind-list-text']")
for el in element:
print(el.inner_text)

scroll_to()

滚动到指定高度

Parameters:

名称类型默认值说明
scroll_topintNot None高度,单位 px
durationint300滚动动画时长,单位 ms

Returns:

  • None

示例代码如下:

1
2
3
4
5
6
7
8
9
10
def test_scroll_to(self):
'''
500ms内页面滚动到高度为200px的位置
:return:
'''
page = self.app.navigate_to("/packageComponent/pages/view/scroll-view/scroll-view")
# 500ms内页面滚动到高度为200px的位置
page.scroll_to(200, 500)
time.sleep(1)
self.assertEqual(page.scroll_y, 200, "scroll success")

wait_for()

等待直到指定的条件成立, 条件可以是页面元素, 也可以是自定义的函数或者是需要等待的时间(单位秒)

Parameters:

名称类型默认值说明
conditionintstrfunction
max_timeoutint10超时时间,单位 s

Returns:

  • bool

示例代码如下:

1
2
3
4
5
6
7
8
def test_wait_for(self):
'''
5秒内等待页面页面元素出现,返回布尔类型
:return:
'''
self.app.navigate_to("/packageComponent/pages/view/view/view")
isTrue = self.page.wait_for("[class='flex-item demo-text-2']", max_timeout=5)
self.assertEqual(True, isTrue, "元素成功加载!")

Element中API的使用

get_element()

查找一个元素

Parameters:

名称类型默认值说明
selectorstrNot None选择器
inner_textstrNone通过控件内的文字识别控件
text_containsstrNone通过控件内的文字模糊匹配控件
valuestrNone通过控件的 value 识别控件
max_timeoutint0超时时间,单位 s

PS: selector 支持的语法:

get_elements()

查找一组元素

Parameters:

名称类型默认值说明
selectorstrNot None选择器
max_timeoutint0超时时间,单位 s
inner_textstrNone通过控件内的文字识别控件
text_containsstrNone通过控件内的文字模糊匹配控件
valuestrNone通过控件的 value 识别控件
indexint-1index==-1: 获取所有符合的元素, index>=0: 获取前index+1符合的元素

PS: 支持的css选择器同 get_element()

Returns:

示例代码如下:

1
2
3
4
5
6
# 一个元素
element = self.page.get_element("selector")
element = element.get_element("selector")
# 一组元素
elements = self.page.get_elements("selector")
elements = element.get_elements("selector")

attribute()

获取元素属性

示例代码如下:

1
2
3
4
5
6
7
8
9
def test_attribute(self):
'''
获取页面元素属性,返回集合
:return:
'''
self.app.navigate_to("/packageComponent/pages/view/view/view")
attribute = self.page.get_element("[class='flex-item demo-text-2']").attribute("class")
print(attribute)
self.assertEqual(['flex-item demo-text-2'], attribute, "元素成功加载!")

tap()

点击元素

click()

tap()之前检查元素pointer-events样式是否为none 示例代码如下:

1
2
3
4
# tap
self.page.get_element('view',inner_text='视图容器',max_timeout=2).tap()
# click
self.page.get_element('view',inner_text='视图容器',max_timeout=2).click()

long_press()

长按元素

示例代码如下:

1
2
# 长按操作
e = page.get_element("#testlongtap") e.long_press()

move()

移动元素(触发元素的 touchstart、touchmove、touchend 事件)

Parameters:

名称类型默认值说明
x_offsetintNot Nonex 方向上的偏移,往右为正数,往左为负数
y_offsetintNot Noney 方向上的偏移,往下为正数,往上为负数
move_delayint350移动前摇,ms
smoothboolFalse平滑移动
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
import minium, time
@minium.ddt_class
class TestElement(minium.MiniTest):
@classmethod
def setUpClass(cls):
super(TestElement, cls).setUpClass()
cls.page = cls.app.redirect_to("/pages/testelement/testelement")

def _reset_movable_view(self):
# 重置元素位置
element = self.page.get_element("movable-view")
element.move_to(0, 0)
time.sleep(1)

def test_move(self):
"""
测试move方法, movable-view元素横向移动30像素, 纵向移动70像素
"""
self._reset_movable_view()
element = self.page.get_element("movable-view")
rect = element.rect
element.move(30, 70, 500) # 横向移动30像素, 纵向移动70像素
self.assertDictEqual(
{
"left": rect["left"] + 30,
"top": rect["top"] + 70,
"width": rect["width"],
"height": rect["height"],
},
element.rect,
)

def test_move_smooth(self):
self._reset_movable_view()
element = self.page.get_element("movable-view")
rect = element.rect
element.move(30, 70, 750, smooth=True)
time.sleep(2)
self.assertDictEqual(
{
"left": rect["left"] + 30,
"top": rect["top"] + 70,
"width": rect["width"],
"height": rect["height"],
},
element.rect,
)

styles()

获取元素的样式属性

Parameters:

名称类型默认值说明
namesstrlistNot None

示例代码如下:

1
2
3
4
5
6
7
8
9
def test_styles(self):
'''
获取元素的样式属性
:return:
'''
self.app.navigate_to("/packageComponent/pages/view/view/view")
attribute = self.page.get_element("[class='flex-item demo-text-2']").styles("color")
print(attribute)
self.assertEqual(['rgb(255, 255, 255)'], attribute, "获取元素的样式属性成功!")

scroll_to()

元素滚动

基础库v2.23.4版本后支持

Parameters:

名称类型默认值说明
topintNonex 轴上滚动的距离
leftintNoney 轴上滚动的距离

示例代码如下:

1
2
3
4
5
6
7
8
9
def test_scroll_to(self):
'''
元素滚动
:return:
'''
self.app.navigate_to("/packageComponent/pages/view/view/view")
x=self.page.scroll_width
y=self.page.scroll_height
self.page.get_element('view',inner_text='B').scroll_to(x/2,y/2)

input()

input & textarea 组件输入文字

IDE上不会改变element上的value属性,建议使用变化的Page.data/hook绑定的input方法判断是否生效

Parameters:

名称类型默认值说明
textstrNone输入文本

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
def test_input(self):
'''
元素输入操作
:return:
'''
# input框
self.app.navigate_to("/packageComponent/pages/form/input/input")
self.page.get_element('[placeholder="最大输入长度为10"]').input("文本内容")
# textarea输入框
self.app.navigate_to("/packageComponent/pages/form/textarea/textarea")
self.page.get_element('.textarea-wrp > textarea').input("文本内容")

常见组件的处理

常见组件的处理

switch组件处理

改变 switch 组件的状态

示例代码如下:

1
2
3
4
5
6
7
def test_switch(self):
'''
switch 组件处理演示,可以脑补下单选框状态切换场景
:return:
'''
self.app.navigate_to("/packageComponent/pages/form/switch/switch")
self.page.get_element(".page-section.page-section-gap > view.body-view > switch:nth-child(1)").switch()

slide组件处理

slider 组件滑动到指定数值

示例代码如下:

1
2
3
4
5
6
7
8
9
10
def test_slide_to(self):
'''
slider组件处理演示
:return:
'''
self.app.navigate_to("/packageComponent/pages/form/slider/slider")
element_slider = self.page.get_element('page > view > view > view:nth-child(1) > view.body-view > slider')
element_slider.slide_to(5)
time.sleep(1)
self.assertEqual(element_slider.value, 5, "slider ok")

pick组件处理

picker 组件选值

Parameters:

名称类型默认值说明
value看下表Not None属性名称

value 的取值:

选择器类型类型说明
selector: 普通选择器int表示选择了 range 中的第几个 (下标从 0 开始)
multiSelector: 多列选择器int表示选择了 range 中的第几个 (下标从 0 开始)
time: 时间选择器str表示选中的时间,格式为"hh:mm"
date: 日期选择器str表示选中的日期,格式为"YYYY-MM-DD"
region: 省市区选择器int表示选中的省市区,默认选中每一列的第一个值

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def test_picker(self):
'''
picker组件处理演示。修改当前时间
:return:
'''
self.app.navigate_to("/packageComponent/pages/form/picker/picker")
callback_called = threading.Semaphore(0) # 监听回调, 阻塞当前主线程

def callback(args):
nonlocal callback_args
callback_args = args
callback_called.release()

els = self.page.get_elements("picker")
self.app.hook_current_page_method('bindTimeChange', callback)
els[1].click() # 阻止picker弹起
els[1].pick('11:18') # 用trigger模拟pick完成的动作

scroll_to组件处理

scroll-view 容器滚动操作

Parameters:

名称类型默认值说明
xintNonex 轴上滚动的距离
yintNoney 轴上滚动的距离

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def test_scroll_to(self):
'''
scroll_to组件处理演示
:return:
'''
self.app.navigate_to("/packageComponent/pages/view/scroll-view/scroll-view")
callback_args = None
callback_called = threading.Semaphore(0) # 监听回调, 阻塞当前主线程

def callback(args):
nonlocal callback_args
callback_args = args
callback_called.release()

# 监听滚动事件, 方便最后验证滚动结果
self.app.hook_current_page_method("scroll", callback)
# 这里只演示的事横向移动,主要找准你要移动的控件,整错了铁定没法移动
els = self.page.get_elements("scroll-view")
els[2].scroll_to(x=150) # 横向滚动150像素
self.assertTrue(callback_called.acquire(timeout=10), "callback called")
self.assertEqual(callback_args[0]["detail"]["scrollLeft"], 150, "pick ok")

swipe组件处理

切换 swiper 容器当前的页面

Parameters:

名称类型默认值说明
indexintNone索引值,从 0 开始

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
def test_swipe_to(self):
'''
swipe组件处理演示
:return:
'''
self.app.navigate_to("/packageComponent/pages/view/swiper/swiper")
el = self.page.get_element("swiper")
# 切换到第二个tab,轮播图的C
el.swipe_to(2)
print(el.attribute('current'))
self.assertEqual(el.attribute('current'), ['2'], "swipe ok")

move组件处理

movable-view 容器拖拽滑动

Parameters:

名称类型默认值说明
xintNonex 轴方向的偏移距离
yintNoney 轴方向的偏移距离

PS: x,y 偏移量相对于*movable-area*左上角,如示例中,*movable-area*左上角为(25, 25)

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
def test_move_to(self):
'''
move组件处理演示
:return:
'''
self.app.navigate_to("/packageComponent/pages/view/movable-view/movable-view")
element = self.page.get_element("movable-view")
# 把movable-view复位
element.move_to(0, 0)
time.sleep(2)
# 移动到坐标为100, 100的地方
element.move_to(100, 100)

video、audio 组件

详见代码示例

video组件处理示例代码如下:

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
def test_video(self):
'''
video组件处理演示
:return:
'''
self.app.navigate_to("/packageComponent/pages/media/video/video")
element_video = self.page.get_element("video")
# 播放
element_video.play()
time.sleep(2)
# 暂停
element_video.pause()
time.sleep(2)
# 跳转到指定位置(拖到50秒位置播放)
element_video.seek(50)
time.sleep(2)
element_video.play()
# 1.5倍速播放
element_video.playback_rate(1.5)
time.sleep(2)
# 进入全屏播放
element_video.request_full_screen(90)
time.sleep(2)
# 发送弹幕
element_video.send_danmu('我是弹幕君!!')
time.sleep(2)
# 退出全屏
element_video.exit_full_screen()
time.sleep(2)
# 播放
element_video.stop()

audio组件处理示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
def test_audio(self):
'''
audio组件处理演示
:return:
'''
self.app.navigate_to("/packageComponent/pages/media/audio/audio")
element_audio = self.page.get_element("audio")
element_audio.play() # 播放音频
element_audio.seek(10) # 快进音频
element_audio.pause() # 暂停音频
element_audio.set_src('http: // ws.stream.qqmusic.qq.com / M500001VfvsJ21xFqb.mp3?guid = ffffffff82def4af4b12b3cd9337d5e7 & uin = 346897220 & vkey = 6292asdad & fromtag = 46') # 设置音源链接

其他示例

单页面示例

直接跳转到被测试的页面,进行脚本的测试。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def test_set_data(self):
'''
单页面示例
:return:
'''
self.app.navigate_to("/packageComponent/pages/content/text/text")
self.page.data = {
'text': "只能加文字,不能删除文字",
'canAdd': True,
'canRemove': False
}
time.sleep(1)
self.capture("canAdd")
self.page.data = {
'text': "只能删除文字,不能加文字",
'canAdd': False,
'canRemove': True
}
time.sleep(1)
self.capture("canRemove")

数据驱动测试

测试框架继承自unittest,基于ddt封装的的简单封装。

示例代码如下:

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
# -*- coding: utf-8 -*-
"""
# @Time : 2023/02/06 20:31
"""
import minium


@minium.ddt_class
class BaseTest(minium.MiniTest):
@minium.exit_when_error
def test_init(self):
"""
这条用例失败会退出测试计划,minium.exit_when_error可以用来修饰初始化用例
"""
self.assertEqual(1, 1)

@minium.ddt_case(1, 2, 3)
def test_ddt(self, value):
"""
数据驱动测试,这个case会自动展开成3条用例:
test_ddt_1_1
test_ddt_2_2
test_ddt_3_3
"""
self.assertIn(value, [1, 2, 3])


if __name__ == '__main__':
BaseTest.test_ddt()

测试框架的设计和开发

框架的设计开发

框架搭建设计要素

  • 日志&测试步骤
  • 报告&失败截图
  • 配置文件&数据源设计
  • 公共函数&API封装
  • 测试数据&参数化、解耦
  • 测试套件&测试用例设计、组装

工程结构

日志

日志可以很好辅助我们定位问题,示例代码如下:

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
class LogUtils:

def __init__(self, log_path=log_path):
"""
通过python自带的logging模块进行封装
"""
self.logfile_path = log_path
# 创建日志对象logger
self.logger = logging.getLogger(__name__)
# 设置日志级别
self.logger.setLevel(level=logging.INFO)
# 设置日志的格式
formatter = logging.Formatter('%(asctime)s - %(filename)s [line:%(lineno)d] - %(levelname)s: %(message)s')
"""在log文件中输出日志"""
# 日志文件名称显示一天的日志
self.log_name_path = os.path.join(self.logfile_path, "log_%s" % time.strftime('%Y_%m_%d')+".log")
# 创建文件处理程序并实现追加
self.file_log = logging.FileHandler(self.log_name_path, 'a', encoding='utf-8')
# 设置日志文件里的格式
self.file_log.setFormatter(formatter)
# 设置日志文件里的级别
self.file_log.setLevel(logging.INFO)
# 把日志信息输出到文件中
self.logger.addHandler(self.file_log)
# 关闭文件
self.file_log.close()

"""在控制台输出日志"""
# 日志在控制台
self.console = logging.StreamHandler()
# 设置日志级别
self.console.setLevel(logging.INFO)
# 设置日志格式
self.console.setFormatter(formatter)
# 把日志信息输出到控制台
self.logger.addHandler(self.console)
# 关闭控制台日志
self.console.close()

def get_log(self):
return self.logger

数据源

这里我用的是Excel,示例如下:

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
class ExcelUtils(object):
@staticmethod
def get_element_Data():
"""
通过pandas读取excel中的数据,返回字典映射
"""
data_list = pd.read_excel(excel_path).values.tolist() # reading file
dict_elements = {}
for data in data_list:
dict_elements[data[0]] = data[1] + "," + data[2]
return dict_elements

可能评论区会有人说用yml、json、csv做数据源会更好,我不认同!

为什么用Excel做数据源?

  • 所有的测试框架和测试工具,都应该以使用者角度考虑问题,以易用性和上手难度为先。
  • 所有做测试工具及平台、测试框架,都是为他人服务,所以越简单,越好操作,更好,后期可以再优化、
  • 上面做数据源,可能自我感觉技术上显得高大上,很牛逼,但是抱歉,使用者,根本不知道yml、json是啥你怎么办,可以学,没错(互联网时代时间成本太昂贵了),不是不可能遇到,是因为最不可控的是使用者人群,不是吗?

框架的一开始设计很重要,所以整体的设计要清晰明了。

感动自己的实现不重要,而是被团队需要的实现,才会显得自己重要!

基础层

这里主要用于处理,元素对象和原生API的封装,部分代码示例如下图:

测试用例

action层写测试用例,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class PageAction(BasePage):

def order(self, taste: str):
"""
根据口味选餐
:param taste:
:return:
"""
# 将第一个五花肉石锅拌饭加入购物车
self.element_click("将第一个五花肉石锅拌饭加入购物车")
# 选择口味
self.element_click(taste)
# 确定选择
self.element_click("确定选择")
# 共选择份数
total = self.get_elementText("共选择份数")
return total

调用action层,执行测试用例,示例代码如下:

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
# -*- coding: utf-8 -*-
"""
# @Time : 2023/03/20 20:55
"""
import minium

from action.page_action import PageAction


@minium.ddt_class
class OrderTest(minium.MiniTest):
"""
测试登录功能
"""
pageAction = None

@minium.ddt_case(
{"taste": "蒜香味", "count": " 1 "},
{"taste": "姜葱味", "count": " 1 "},
{"taste": "盐焗味", "count": "3"}
)
def test_Order(self, value):
try:
self.pageAction = PageAction(self.mini, self.page)
total = self.pageAction.order(value["taste"])
self.assertEqual(total, value["count"])
except AssertionError as err:
self.pageAction.screen_shot()
self.fail(err)

测试报告

觉得minium的测试报告颜值还可以,还可以看到历史的,感觉还不错,如下:

失败有截图还有日志: