一、需求背景:
[群接龙]小程序有各种针对社群开发的活动,轻量应用,使用简单。以及有针对各类社群开发的社群主页,帮助用户轻松经营好社群。特别在社区/社群团购领域,[群接龙]是最有影响力的产品之一。
目前,群接龙小程序用户数量已经达1.8亿+,并且小程序的功能每周都在迭代更新,为保证用户的体验质量,我们需要在每次发版前,对上传的体验版进行回归,依赖手动回归并不现实,因此我们选择使用Minium+云测,来实现小程序自动化,提高回归效率,保障产品质量。
二、技术方案
使用Python+Minium+云测编写测试用例,并将测试报告发送到企微。
三、环境搭建
环境搭建本文中不做展开,参考 官方文档 即可
四、使用Minium编写测试用例
1.框架介绍
- base:封装页面公共方法和测试基类的封装
- common:存放公共方法,如读取脚本
- data:存放一些测试数据
- page:生成的页面路径,维护各页面的元素和操作方法
- script:通过开发者工具录制导出的自动化脚本
- testcase:测试用例
- tools:存放工具方法
- 在base/def中,我们封装了一些minium框架的方法,如页面跳转,点击元素,文本输入方法等,可以根据自己的需求进行封装。
import base64
import os
from pathlib import Path
from time import sleep
import minium
import requests
from minium import Callback
class BaseDef(minium.MiniTest):
def __init__(self, mini):
super().__init__()
self.mini = mini
def navigate_to_open(self, route):
"""以导航的方式跳转到指定页面,不允许跳转到 tabbar 页面,支持相对路径和绝对路径, 小程序中页面栈最多十层"""
self.mini.app.navigate_to(route)
def redirect_to_open(self, route):
"""关闭当前页面,重定向到应用内的某个页面,不允许跳转到 tabbar 页面"""
self.mini.app.redirect_to(route)
def relaunch_to_open(self, route):
"""关闭所有页面,打开到应用内的某个页面"""
self.mini.app.relaunch(route)
# 判断某个元素是否存在
def element_is_exists(self, element):
self.mini.logger.info(f"目前在断言元素{element}")
bool = self.mini.page.element_is_exists(element)
try:
assert bool == True
except AssertionError:
self.mini.logger.error(f"断言失败,错误元素{element}")
raise AssertionError(f"断言失败,错误元素{element}")
# 文本框输入
def send_key(self, element, text: str):
sleep(1)
try:
for i in range(10):
ele = self.mini.page.wait_for(element, max_timeout=3)
if ele:
self.mini.logger.info(f"目前在输入元素{element}")
ele_text = self.page.get_element(element)
ele_text.input(text)
return
else:
self.mini.logger.error(f"找不到该元素{element},无法点击!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
raise RuntimeError(f"找不到该元素{element},超过等待时间,用例执行失败")
except AttributeError as e:
self.mini.logger.error(f"找不到该元素{element},无法输入!!!,,报错原因{e}")
raise
def tap(self, element):
"""
:param element: 要点击的元素
:return:
"""
sleep(1)
try:
for i in range(10):
ele = self.mini.page.wait_for(element, max_timeout=3)
if ele:
self.mini.logger.info(f"目前在点击元素{element},点击方式Tap")
ele_tap = self.mini.page.get_element(element)
ele_tap.tap()
sleep(0.7)
return
else:
self.mini.logger.error(f"找不到该元素{element},无法点击!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
raise RuntimeError(f"找不到该元素{element},超过等待时间,用例执行失败")
except AttributeError as e:
self.mini.logger.error(f"找不到该元素{element},无法输入!!!,,报错原因{e}")
raise
- base/basepage,是测试用例的基类,这里可以封装一些用例执行之前的操作
import minium
from common.del_outputs import del_outputs
from base.basedef import *
class BasePage(minium.MiniTest):
"""测试用例基类"""
@classmethod
def tearDownClass(cls):
pass
@classmethod
def setUpClass(cls):
pass
def setUp(self):
pass
def tearDown(self):
pass
- common/read_script,此函数的作用是读取自动化脚本中的元素和页面路径,打印元素和创建页面
import json
import os
#上传到云测的时候,需要把下面两行注释,和注释函数调用
object_path = os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
file = os.path.join(object_path,"script/script.json").replace("\\","/")
def get_path(file):
#用于获取当前目录的根目录路径
def find_project_root():
current_dir = os.getcwd()
while not os.path.exists(os.path.join(current_dir, 'README.md')):
if current_dir == os.path.dirname(current_dir):
return None # 达到文件系统根目录但未找到项目根目录
current_dir = os.path.dirname(current_dir)
return current_dir.replace('\\', '/')
root = find_project_root()
with open(file,encoding="UTF-8") as f:
data = json.load(f)
#读取脚本文件中的commands字段
for path in data['commands']:
if 'path' in path:
#获取path字段的值,用于生成目录
path_parts = path["path"].split('/')
current_path = os.path.join(root,"pages")
existing_directories = set()
for part in path_parts:
if part not in ['pro', 'page' , 'homepage']:
#更改文件名,python文件名不能用-起名
part = str(part).replace("-","_")
current_path = os.path.join(current_path, part)
#判断文件夹是否存在
if part not in existing_directories:
os.makedirs(current_path, exist_ok=True)
py_file = os.path.join(current_path, f"{part}.py")
#判断文件是否存在
if os.path.exists(py_file):
pass
else:
open(py_file, 'w').close()
existing_directories.add(part)
#获取target元素和文字
def get(file):
with open(file,encoding="UTF-8") as f:
data = json.load(f)
for command in data['commands']:
if 'target' in command and command["command"] == "tap":
target = command['target']
text = command["text"]
path = command["path"]
print("按钮名称:"+text,"\n")
print("所属页面"+path,"\n")
print("xpath表达式:"+target,"\n")
elif 'target' in command and command["command"] == "input":
input = command["target"]
print("input输入框按钮:"+input,"\n")
get(file)
get_path(file)
- tools/cloud_test_output.py,用于创建云测测试任务,并将测试报告发送到企微
云测第三方接口文档:云测文档 ,需要将一下代码中的token、group_en_id、test_plan_id、企微机器人路径替换成自己的
import json
import time
import requests
from urllib.parse import urlparse, parse_qs
class MiniTestApi:
def __init__(self, user_token, group_en_id):
self.token = user_token # 需要填写自己的token
self.group_en_id = group_en_id # 项目的英文ID
self.minitest_api = 'https://minitest.weixin.qq.com/thirdapi'
def third_auto_task(self):
"""
创建测试任务
:return:
"""
config = {
"assert_capture": True,
"auto_relaunch": False,
"auto_authorize": False,
"audits": False,
"compile_mode": ""
}
data = {
'token': self.token,
'group_en_id': self.group_en_id,
'test_type': 2,
'platforms': 'ios',
'wx_id': '',
# 跑体验版
'wx_version': 2,
'desc': 'Minium测试',
#换成云测上的测试计划ID
'test_plan_id': xx,
'dev_account_no': 1,
'minium_config': config,
#换成测试的账号名称
"virtual_accounts": "xx"
}
resp = requests.post(
self.minitest_api + '/plan',
json=data
)
resp = resp.json()
print(resp)
return resp["data"]["plan_id"]
def share_url(self, planId):
"""
分享测试报告
:param planId:
:return:
"""
data = {
'token': self.token,
'group_en_id': self.group_en_id,
'plan_id': planId,
}
resp = requests.get(
self.minitest_api + '/share_url',
params=data
)
resp = resp.json()
return resp["data"]["share_url"]
def add_case_plan(self):
"""
新增测试计划
:return {'msg': "添加测试计划成功", 'rtn': 0}
"""
plan_config = {
"pkg_list": [
{
"case_list": [
"test_*"
],
"pkg": "testcase.*"
}
]
} # 参照文档 https://minitest.weixin.qq.com/#/minium/Python/framework/suite 进行编写
data = {
'token': self.token,
'group_en_id': self.group_en_id,
'test_plan_name': 'api自定义Minium', # 测试计划名称
'test_plan_config': json.dumps(plan_config)
}
resp = requests.post(url=self.minitest_api + '/case_plan', json=data)
print(resp.json())
return resp.json()
def get_task(self, plan_id):
"""
获取云测任务的执行结果
:param plan_id:
:return:
"""
data = {
'token': self.token,
'group_en_id': self.group_en_id,
'plan_id': plan_id,
}
max_retries = 10
retry_count = 0
while retry_count < max_retries:
try:
resp = requests.get(
self.minitest_api + '/plan',
params=data
)
res = resp.json()
status_text = res["data"]["status_text"]
if status_text == "测试结束":
report_url = minitest_client.share_url(plan_id)
create_time = res["data"]["create_time"]
finish_time = res["data"]["finish_time"]
total_case_num = res["data"]["total_case_num"]
success_case_num = res["data"]["success_case_num"]
minitest_client.sendMessge(report_url, create_time, finish_time, total_case_num, success_case_num)
print("测试完成")
break
else:
print("还在测试中,等待...")
retry_count += 1
if retry_count < max_retries:
time.sleep(300)
except Exception as e:
print("发生异常:", str(e))
retry_count += 1
if retry_count < max_retries:
time.sleep(300)
else:
raise e
return None
def sendMessge(self, report_url, create_time, finish_time, total_case_num, success_case_num):
"""
:param passed: 通过的用例数
:param failed: 失败的用例数
:param broken: 报错的用例数
:return:
"""
#替换自己成企微机器人的地址
Webhook = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=XX"
headers = {'Content-Type': 'application/json'}
data = {
"msgtype": "markdown",
"markdown": {
"content":
'''提醒!前端UI自动化测试反馈\n请相关同事注意,及时跟进!\n
> 云测报告链接:[minium 测试报告,请点击后进入查看]({})
> 用例开始时间:{}\n
> 用例结束时间:{}\n
> 用例总数:{}\n
> 通过用例数:{}\n
> 失败用例数:{}\n
'''.format(report_url, create_time, finish_time, total_case_num, success_case_num,
(total_case_num - success_case_num))
}
}
requests.post(url=Webhook, headers=headers, json=data)
if __name__ == '__main__':
#填写自己的账号token
minitest_client = MiniTestApi('XX', 'XX')
print("开始创建测试任务")
plan_id = minitest_client.third_auto_task()
print("查询任务状态")
minitest_client.get_task(plan_id)
2.通过一个例子学会如何编写自动化用例
我们通过群接龙小程序中,从主页进入商品库中添加商品的用例,来介绍整个用例编写流程。
点击商品库 → 添加商品 → 确认添加
- 首先通过IDE自带的自动化测试功能,完成小程序用例的录制并导出用例
- 导出脚本后,获得一个json文件,将json文件替换掉目录script/script.json的文件,执行common/read_script.py,生成目录和获取元素
运行完后,我们可以看到控制台打印了我们脚本中的元素XPATH定位和生成了页面路径和py文件,接下来我们只需要去生成的py中,创建一个页面的类,并在类中维护页面元素和操作方法即可。
这里我们将使用xpath作为元素的定位方式:
优点:脚本直接导出生成,定位精准,免去调试元素的时间,不懂定位方法也可以编写用例,只要懂复制粘贴就行。
缺点:只要页面元素改动,就需要重新进行元素维护
因为编写自动化的页面一般是不常改动的页面,所以我们选择用xpath,就算改动了,重新再录一遍导出就行,也可以配合其他选择器定位灵活使用,根据自己的需求来。
编写页面类,每个页面只做当前页面的操作
主页:
from base.basedef import BaseDef
from data import page_data
class GroupHomepage(BaseDef):
produnct_warehouse = "//*[text()='商品库']"
def click_produnct_warehouse(self):
self.tap(self.produnct_warehouse)
商品库页:
from base.basedef import BaseDef
class ProductWarehouse(BaseDef):
addGoods = "text.little-font.flexShrink.ml12"
def click_add_goods(self):
self.tap(self.addGoods)
商品创建页:
from base.basedef import BaseDef
class ProductSetting(BaseDef):
goods_name = "/page-wrapper/view/view/view/view[1]/info-form/view/view[1]/view/view/view/input"
goods_multi = "/page-wrapper/view/view/view/view[3]/info-form/view/view[1]/view/view[2]/view/input"
goods_price = "/page-wrapper/view/view/view/view[3]/info-form/view/view[2]/view/view[2]/view/input "
confirm = "/page-wrapper/view/view/view/footer-button/view[2]/view/view"
def add_goods(self):
self.send_key(self.goods_name,"我是单规格商品")
self.send_key(self.goods_multi ,"我是规格")
self.send_key(self.goods_price,"10")
self.tap(self.confirm)
编写测试用例,测试用例需要test_xx开头,不然读取不到用例:
from base.basepage import BasePage
from pages.pages.group_homepage.group_homepage import GroupHomepage
from pages.pages.seq_manage_options.product_setting.product_setting import ProductSetting
from pages.pages.seq_manage_options.product_warehouse.product_warehouse import ProductWarehouse
class TestAddGoods(BasePage):
def __init__(self, methodName='runTest'):
super().__init__(methodName)
self.GroupHomepage = GroupHomepage(self)
self.ProductWarehouse = ProductWarehouse(self)
self.ProductSetting = ProductSetting(self)
"""添加货源单规格商品"""
def test_add_goods_single(self):
self.GroupHomepage.click_produnct_warehouse()
self.ProductWarehouse.click_add_goods()
self.ProductSetting.add_goods()
到此,我们就完成了一个自动化用例的编写,是不是很简单!
五、上传Minium用例到云测,创建云测测试计划,并发生测试报告到企微
云测使用我们不做过多展开,可以参考官方文档使用
上传用例完成后,我们可以通过手动在云测上执行测试任务,或者创建云测定时任务来执行minium用例
我们小程序是在打包完成后调用tools/cloud_test_output.py,来创建测试任务并发生测试报告,也可以将cloud_test_output.py放入Jenkins中进行持续集成,这个根据自己的场景来使用,展示一下最终效果:
最后感谢@kennyyan和@冯永鹏两位微信小伙伴的耐心指导,完结撒花❀
附上云测最佳实践文档:https://developers.weixin.qq.com/community/business/doc/000a0c1d4d41a8e9e7506487c6b40d
感谢分享
感谢分享 ~