@TOC
前言
公司要求做小程序的自动化,最开始让我用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. 云测
六、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——右上角菜单转发分享
源码
附源码链接,有需要可以下载
写的特别棒,有大框架有细节,帮助良多
金刚位是什么?谁可以解释一下
感谢分享,帮助良多,另外我看了下minium文档,执行每个用例前如果不需要跳转到首页,直接在config里面指定:"auto_relaunch": false 即可
BaseCase和BaseDef为啥要分开写的,不能同时都写到BasePage里么
小程序的webview是怎么测试的,目前没有找到方法,虽然通过webview调试页找到了元素,但是程序无法识别