irpas技术客

微信小程序UI自动化实践:python+minium+PO模式(超详细教程附源码供下载)_诗洒趁年华_小程序ui自动化

网络投稿 603

文章目录 前言一、minium介绍二、安装环境1. 安装minium doc2. 安装minium3. 启动小程序 三、准备知识1. 启动2. 配置3. 命令行运行4. 元素定位5. 断言6. 第一个测试用例 四、用例设计模式1. 什么是PO模式2. 层级关系3. 优点 五、框架1. 目录结构2. BaseCase基类3. BaseDef公共方法类4. /pages页面对象类4. /tests测试用例类5. config.json配置6. suite.json测试计划配置7. 执行测试计划8. 查看测试报告9. 真机测试10. 云测 六、ddt数据驱动1. 什么是数据驱动2. 数据驱动的优点3. 使用详解 七、遇到的问题1. page.scroll_to滚动无效2. 前一条用例失败会影响后一条用例的执行3. miniddt修改用例名称4. 小程序右上角菜单转发 源码


前言

公司要求做小程序的自动化,最开始让我用jest+SDK做,但后面学了两天,困难重重,第一,这种方式只支持JS语言,我以前从来没学过JS语言,JS一窍不通;第二,网上资料太少太少了,本来对JS就不懂,网上资料还少,后面去网上百度了下,minium支持python语言,我原来有一点python基础,所以最后改用了python+minium框架。


一、minium介绍

minium提供一个基于unittest封装好的测试框架,MiniTest是minium中继承自unittest.TestCase的测试基类, 你可以在testcase中使用框架实例化好的Minium/App/Native实例,也可以使用unittest中的各种断言函数,并做了以下改动: 1、加载读取测试配置 2、在合适的时机初始化minium.Minium、minium.App和minium.Native 3、根据配置打开IDE,拉起小程序项目或自动打开真机调试 4、拦截assert调用,记录检验结果 5、记录运行时数据和截图,用于测试报告生成


二、安装环境 1. 安装minium doc

(这个主要是minium框架的一些介绍,可以略过不安装直接去官网查看文档:https://minitest.weixin.qq.com/#/minium/Python/readme) (1)该文档使用 docsify 框架,需先安装 docsify

npm i docsify-cli -g

(2)下载文档(会要求填写账号密码,指的是微信代码管理的账号和密码)

git clone https://git.weixin.qq.com/minitest/minium-doc

(3)安装依赖

cd minium-doc npm install

(4)本地部署(浏览器能浏览http://localhost:3000/说明文档安装好了)

docsify serve .

2. 安装minium

(1)运行环境 Python 3.8及以上 微信开发者工具安全模式: 设置 -> 安全设置 -> 服务端口: 打开 确认微信公共库版本 >= 2.7.3 (2)安装 下载minium安装包, 解压后进入文件夹, 运行

python3 setup.py install 3. 启动小程序 import minium class TestMiniprogram(minium.MiniTest): def test_01(self): pass

命令行运行脚本

minitest -c config.json -m tests.igtest
三、准备知识 1. 启动

minium.MiniTest类里面已经封装好了小程序的启动、关闭、调用配置、执行测试用例等一系列的方法,所以我们编写测试用例脚本的时候,定义的类在继承minium.MiniTest类之后,可以直接写测试用例,无需关注怎么启动。

2. 配置

minium框架里面默认配置的项目路径以及CLI工具路径都为None,所以会加载默认配置,如果我们的项目路径以及CLI工具路径不是用的默认路径,执行会报错找不到路径,所以我们需要在项目路径下新建一个config.json文件,将里面的project_path改为你的小程序项目路径,dev_tool_path改为你的CLI工具路径

3. 命令行运行

相关字段的说明

minitest -c config.json -m tests.igtest -g -c 指定配置文件 -m 指定要执行的用例文件名(注意不需要.py) -g 生成测试报告 4. 元素定位

(1)单选择器定位:一般可以使用.class或者#id去定位到元素 (2)多选择器定位:如果元素class有重名,id也有相同的,可以使用.class+#id去定位 (3)组合定位:如果有多个元素的class相同,id又是变化的,可以使用page.get_element(‘.main-menu-txt’, inner_text=’租赁合同’, text_contains=‘租赁合同’),或者使用page.get_elements(‘.main-menu-txt’)[序号]

5. 断言

常用的断言主要有三种: (1)assertEqual(first, second, msg) first == second时,断言成功,用例结果符合预期 first != second时,断言失败,抛出错误信息及msg (2)assertTrue(expr, msg) expr为True,断言成功,用例结果符合预期 expr为False,断言失败,抛出错误信息及msg (3)assertTexts(texts, selector, msg) texts中每个元素的值都包含在selector选择器对应的元素文本集合中,则断言成功,否则,断言失败,抛出错误信息及msg

6. 第一个测试用例 import minium class TestIg(minium.MiniTest): def test_01_login(self): self.mini.page.wait_for(2) # 当前页面等待2s self.mini.page.get_element('.login-word-input').input(user) # 输入用户名 self.mini.page.get_element('.login-pass').input(password) # 输入密码 self.mini.page.get_element('.loginBtn').click() # 点击登录按钮 self.mini.page.wait_for(3) # 当前页面等待3s self.assertEqual('/pages/main/index', self.mini.app.current_page.path, msg='登录失败') # 断言登录后是否跳转到首页,如果断言失败,则返回mgs"登录失败" minitest -c config.json -tests.igtest 四、用例设计模式

上面第一个测试用例写到了登录,非常简单常见的一个场景,但像我在做的这个项目,在登录后,跳转到首页后有项目切换、项目信息、项目试算、租赁订单、租赁合同等等非常多模块的定位、元素点击、页面滑动、页面跳转以及断言等,把这么多代码全部写到一个类里面,代码多而繁杂,如果页面元素有更改,或者需要增加用例,查找起来非常不方便,更不要说整个小程序内有那么多页面,所以需要进行分层管理,我使用的是PO模式

1. 什么是PO模式

PO模式,即page object mode,页面对象模式,通过对界面元素和功能模块的封装减少冗余代码,同时在后期维护中,若元素定位或功能模块发生变化,只需要调整页面元素或功能模块封装的代码,提高测试用例的可维护性。

2. 层级关系

第一层:基础层BasePage,作用:封装一些minium的原生方法,如元素定位、框架跳转等 第二层:PO层,页面对象层,如元素定位、获得元素对象、页面操作 第三层:测试用例层,主要负责业务逻辑和数据驱动

三层之间的关系:PO层继承基础层的类,测试用例层调用PO层

3. 优点

(1)分层清晰,易读 (2)易维护,由于元素定位与业务逻辑的分层特性,元素定位变动只需要修改PO层的元素定位,不需要修改业务逻辑,容易维护 (3)可复用,由于在基础层已经对minium的原生方法进行过二次封装,所以方法具有一定的通用性,可复用(但这取决于二次封装方法的通用性有多高)

五、框架 1. 目录结构

(1)/cases/base/ /cases/base/basecase:测试用例基类,用于设置用例输出路径和清理工作,项目的测试用例都继承此类 /cases/base//basedef:页面基类,封装所有页面会用到的公用方法 /cases/base/router:小程序内各页面路径 (2)/cases/pages 页面对象模型:获得元素对象、页面操作等 (3)tests包 各页面的测试脚本 /tests/outputs 测试报告:记录各测试用例的执行情况 /tests/config.json 配置信息,包括小程序项目地址、cli工具地址等 /tests/suite.json 测试计划,配置要执行的用例及执行顺序

2. BaseCase基类

(1)复写minium.MiniTest类里面的setUpClass、tearDownClass、setUp、tearDown方法 setUpClass、tearDownClass前面一定要加@classmethod修饰器,这两个方法在整个测试计划执行期间只会执行一次,setUp、tearDown在每个测试用例case执行前后都会执行,minium.MiniTest内的setUp方法会将小程序恢复到默认页面,即小程序首页 (2)因为我写了很多页面,如果在每个页面里面的每条用例执行前都恢复到小程序首页,会大大增加用例执行时间,所以我复写的setUp、tearDown方法都直接pass了

from pathlib import Path import minium class BaseCase(minium.MiniTest): """测试用例基类""" @classmethod def setUpClass(cls): super(BaseCase, cls).setUpClass() output_dir = Path(cls.CONFIG.outputs) if not output_dir.is_dir(): output_dir.mkdir() @classmethod def tearDownClass(cls): super(BaseCase, cls).tearDownClass() def setUp(self): pass def tearDown(self): pass 3. BaseDef公共方法类

定义一些公共方法

class BaseDef: def __init__(self, mini): self.mini = mini '''跳转到指定页面''' def navigate_to_open(self, route): self.mini.app.navigate_to(route) '''跳转到指定页面并关闭当前页面''' def redirect_to_open(self, route): self.mini.app.redirect_to(route) '''跳转到tabbar页面,关闭其他非tabbar页面''' def switch_to_tabbar(self, route): self.mini.app.switch_tab(route) '''跳转到非原生tabbar页面''' def switch_to_not_tabbar(self, selector, str=None): self.mini.page.get_element(selector, inner_text=str).click() '''无需加载的页面滑动到页面底部''' def scroll_to_buttom(self, selector): el = self.mini.page.get_element('scroll-view') rect = self.mini.page.get_element(selector).rect el.scroll_to(y=rect['top']) '''元素列表中找第几个元素''' def get_el_in_els(self, selector, index=0): els = self.mini.page.get_elements(selector) return els[index] '''断言每个元素文本中都包含某元素''' def assertIn(self, selector, text, msg=None): els1 = self.mini.page.get_elements(selector, text_contains=text) els2 = self.mini.page.get_elements(selector) if els1 != els2: raise AssertionError("selector:%s, inner_text=%s not Found") @property def current_path(self): return self.mini.page.path 4. /pages页面对象类

里面写了每个页面的元素定位,页面操作 (1)homepage:首页相关的元素定位、页面操作

from business_test.cases.base import router, basedef class HomePage(basedef.BaseDef): locator = { 'top_area': '.true-top-menu-item', # 金刚位 'main_area': '.main-menu-item', # 功能位 'project': '.content', # 项目切换 'project_select': '.list-cell' # 项目选择 } # 校验首页页面路径 def check_homepage_path(self): self.mini.page.wait_for(2) self.mini.assertEqual(self.current_path, router.homePage) # 根据index,点击金刚位的项目卡片、试算助手、联系人、流程中心 def toparea_hop(self, index=0): els = self.mini.page.get_elements(self.locator['top_area']) els[index].click() # 根据序号,点击功能位的租赁订单、租赁合同…… def mainarea_hop(self, index=0): els = self.mini.page.get_elements(self.locator['main_area']) els[index].click() # 当前项目名称 def current_project_name(self): self.mini.page.wait_for(self.locator['project'], max_timeout=3) return self.mini.page.get_element(self.locator['project']).inner_text # 跳转到项目选择页面 def project_select_page(self): self.mini.page.wait_for(self.locator['project'], max_timeout=3) self.mini.page.get_element(self.locator['project']).click() self.mini.page.wait_for(3) return self.mini.app.current_page # 选择项目 def project_selector(self, project_name=''): page = self.mini.app.current_page page.wait_for(self.locator['project_select'], max_timeout=3) if project_name == '': return page.get_elements(self.locator['project_select']) else: return page.get_element(self.locator['project_select'], inner_text=project_name)

(2)customerpage:客户管理页面相关的元素定位、页面操作

略。。。 4. /tests测试用例类

(1)homepage_test:首页相关的测试用例

from business_test.cases.pages import loginpage from business_test.cases.pages import homepage from business_test.cases.base.basecase import BaseCase from business_test.cases.base import router, accounts from random import randint import minium # 小程序首页测试 @minium.ddt_class() class HomePageTest(BaseCase): def __init__(self, methodName = 'runTest'): super(HomePageTest, self).__init__(methodName) self.homePage = homepage.HomePage(self) def test_00_login_sure(self): if self.app.current_page.path == router.loginPage: loginpage.LoginPage(self).login(user=accounts.user, password=accounts.password) else: pass def test_01_homepagePath(self): """验证首页路径正确""" self.homePage.check_homepage_path() def test_02_modulesExist(self): """验证首页相关模块存在""" self.assertTexts(['到期房源', '项目商机', '签约概览'], 'view', '到期房源、项目商机、签约概览模块存在') @minium.ddt_case( (0, '首页—>项目卡片'), (1, '首页—>试算助手'), (2, '首页—>联系人'), (3, '首页—>流程中心') ) def test_03_topAreaPath(self, args): """验证首页金刚位入口正确跳转""" paths = [router.projectinfoPage, router.trialPage, router.contactPage, router.processPage] self.homePage.toparea_hop(index=args[0]) self.page.wait_for(2) try: self.assertEqual(paths[args[0]], self.app.current_page.path, msg=args[1]) self.app.navigate_back() except: self.app.navigate_back() @minium.ddt_case( (0, '首页—>租赁订单列表'), (1, '首页—>租赁合同列表'), (2, '首页—>认购单列表'), (3, '首页—>销售合同列表'), (4, '首页—>企业动态'), (5, '首页—>企业宝典'), (6, '首页—>租赁需求列表'), (7, '首页—>销售需求') ) def test_04_mainAreaPath(self, args): """验证首页功能位入口正确跳转""" paths = [router.rentPage, router.rentPage, router.salePage, router.salePage, router.dynamicPage, router.biblePage, router.demandPage, router.demandPage] self.homePage.mainarea_hop(index=args[0]) self.page.wait_for(2) try: self.assertEqual(paths[args[0]], self.app.current_page.path, msg=args[1]) self.app.navigate_back() except: self.app.navigate_back() def test_05_switchProjectPath(self): """验证项目选择页面路径正确""" self.homePage.project_select_page() try: self.assertEqual(router.selectProjectPage, self.app.current_page.path) self.app.navigate_back() except: self.app.navigate_back() def test_06_switchProject(self): """验证随机切换项目成功""" self.homePage.project_select_page() projects = self.homePage.project_selector() selection = projects[randint(0, len(projects) - 1)] project_name = selection.inner_text selection.click() self.assertEqual(project_name, self.homePage.current_project_name(), msg='当前项目为随机选择项目') def test_07_reset(self): """一个页面测试完成,重置到首页InfiPark项目""" self.homePage.redirect_to_open(router.homePage) self.homePage.project_select_page() self.homePage.project_selector(project_name='InfiPark').click() self.assertEqual(self.homePage.current_project_name(), 'InfiPark')

(2)customerpage_test:客户管理相关的测试用例

略。。。 5. config.json配置 { "project_path": "C:/Users/LX/Desktop/mini/cloud-merchant-mini-programs", # 项目路径 "dev_tool_path": "D:/微信开发者工具/微信web开发者工具/cli.bat", # CLI工具路径 "debug_mode": "debug", # 日志打印级别 "test_port": 9420, # 端口号 "platform": "ide", # 测试平台,有ide、Android、IOS "app": "wx", "assert_capture": true, "request_timeout": 60, "remote_connect_timeout": 300, "auto_relaunch": false, "enable_app_log": true } 6. suite.json测试计划配置

配置需要执行的用例以及用例执行顺序,根据pkg去匹配包名,找到测试类,然后再根据case_list里面的规则去查找测试类的测试用例。 在suite.json中,未明确指定用例的执行顺序时,用例是按照用例名称升序去执行的,所以定义用例名称时,最好在test后加上数字,保证用例按照我们书写的顺序执行

{ "pkg_list": [ { "case_list": [ "test_*" # 用例名称,*是通配符,意思是执行以test_开关的测试用例 ], "pkg": "tests.homepage_test" # 用例文件名,不需要加.py }, { "case_list": [ "test_*" ], "pkg": "tests.customerpage_test" } ] } 7. 执行测试计划 minitest -c config.json -s suite.json -g # 按照suite.json测试计划执行,输出报告 minitest -c config.json -m tests.homepage_test --case test_03_moduleExist # 执行homepage_test里面的test_03_moduleExist用例 8. 查看测试报告

用例执行完成后,会自动生成测试报告,相关数据存放在outputs目录下,目录下面有一个index.html文件,但是我们不能直接用浏览器打开这个文件,需要把这个目录放到一个静态服务器上

运行命令 python -m http.server 1234 -d outputs 然后在浏览器上访问http://localhost:1234即可查看报告

9. 真机测试

真机上测试其实是通过开发者工具的真机调试来运行的,所以还是要用到开发者工具 (1)电脑上安装adb,之后命令行运行adb devices获取设备号

C:\windows\system32>adb devices List of devices attached 9YS0220820019844 device

(2)修改config.json文件 (3)与模拟器一样命令行运行就可以了

10. 云测

基于minium框架的小程序UI自动化云测

六、ddt数据驱动 1. 什么是数据驱动

数据驱动,指在自动化测试中处理测试数据的方式。 通常测试数据与功能函数分离,存储在功能函数的外部位置。在自动化测试运行时,数据驱动框架会读取数据源中的数据,把数据作为参数传递到功能函数中,并会根据数据的条数多次运行同一个功能函数。

2. 数据驱动的优点

(1)减少重复代码 比如常见的登录场景,登录可以设计两条用例,登录失败与登录成功,区别只是输入(账号、密码)与输出(跳转路径)不同,常规写法就是写两条用例,test_login_success与test_login_fail,但其实他们有很多重复的代码,如果有的场景,设计的用例非常多,那重复代码也将会非常多,所以引入了ddt数据驱动。 (2)测试用例之间数据隔离 其中一条用例失败,不会影响其他用例的执行

3. 使用详解

minium框架中已经封装好了ddt,只需要在类前加上@minium.ddt_class,用例test_login前面加上@minium.ddt_case((arg1), (arg2), (arg3), ……),最后ddt_case里面有多少参数,用例就会被展开成多少条,在test_login(self, args)中,依次读取ddt_case中的参数,第一条用例中,args=arg1,第二条用例中,args=arg2,第三条用例中,args=arg3……,这样,我们就可以只写一个方法,达到完成两条甚至多条测试用例的目的

from business_test.cases.base.basecase import BaseCase from business_test.cases.pages import loginpage from business_test.cases.base import router, accounts import minium @minium.ddt_class() class LoginPageTest(BaseCase): def __init__(self, methodName='runTest'): super(LoginPageTest, self).__init__(methodName) self.loginPage = loginpage.LoginPage(self) @minium.ddt_case( (0, '登录失败-账号/密码错误', 'aiyumei', '200'), (1, '登录成功-账号:aiyumei', accounts.user, accounts.password) ) def test_login(self, args): if self.app.current_page.path == router.loginPage: paths = [router.loginPage, router.homePage] self.loginPage.login(user=args[2], password=args[3]) self.page.wait_for(3) self.assertEqual(paths[args[0]], self.mini.app.current_page.path, msg=args[1]) elif self.app.current_page.path == router.homePage: pass 七、遇到的问题 1. page.scroll_to滚动无效

页面滚动方法page.scroll_to无效,有时候开发在滚动的时候,会在上面加一层scroll-view组件,导致页面直接滚动无效,需要先定位到scroll-view,再以该元素进行滚动scroll_to操作

2. 前一条用例失败会影响后一条用例的执行

同一个页面,两条用例之间可能会有关联,比如下面的test_05跟test_06,涉及到两个页面,首页和项目选择页面,test_05跟test_06的初始页面都是首页,如果test_05断言失败的话,就不会执行后面的“self.app.navigate_back()”返回首页操作,导致test_06的初始页面不对,test_06也会执行失败,所以需要用到try…except

def test_05_switchProjectPath(self): """验证项目选择页面路径正确""" self.homePage.project_select_page() try: self.assertEqual(router.selectProjectPage, self.app.current_page.path) self.app.navigate_back() except: self.app.navigate_back() def test_06_switchProject(self): """验证随机切换项目成功""" self.homePage.project_select_page() projects = self.homePage.project_selector() selection = projects[randint(0, len(projects) - 1)] project_name = selection.inner_text selection.click() self.assertEqual(project_name, self.homePage.current_project_name(), msg='当前项目为随机选择项目') 3. miniddt修改用例名称

参考该文章:修改miniddt生成用例的名称

4. 小程序右上角菜单转发

参考该文章:小程序自动化框架minium——右上角菜单转发分享

源码

附源码链接,有需要可以下载

https://download.csdn.net/download/baguenaudier/85819436


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #小程序ui自动化 #安装minium #安装minium3