本文为博主原创,未经授权,严禁转载及使用。
本文链接:https://blog.csdn.net/zyooooxie/article/details/123916767
怎么也想不到,我又开始写app自动化。。。 这次,是使用uiautomator2来搞。
【实际这篇博客推迟发布N个月】
个人博客:https://blog.csdn.net/zyooooxie
【以下所有内容仅为个人项目经历,如有不同,纯属正常】
uiautomator2
https://pypi.org/project/uiautomator2/
https://github.com/openatx/uiautomator2
需求
- 使用uiautomator2;
- 用例是有 2个app + 手机浏览器访问某些H5页面,总条数在30条左右;
2个APP:其中一个是内地版,另一个是港澳台定制版。
- 港澳台定制版app 要使用不同账号登录、退出;
港澳台定制版 主要验证的地区是 香港、台湾。
- 用例的前、后置要用 pytest的fixture + 普通setup和teardown;
- 2个app都要执行 新的卸载、安装;
框架设计
使用:uiautomator2+ pytest + allure + pymysql + requests
按理说,pymysql + requests 是用不上的,但实际用例执行前的预置、执行后数据清理 要用到的。
- 清理本地文件(截图),日志不清理;
- 安装App、用户登录 or 浏览器打开URL、用户登录;
- po模式,执行用例、 断言;
- 退出登录、卸载App;
- 生成测试报告【非必要】;
因为生成报告、定时执行 是在大佬的平台上来做,所以我需要做的主要是 前4步。
详细设计
本期内容
本期主要是说 common_function.py、common_variable.py
上图中 common_mysql.py 可以看 https://blog.csdn.net/zyooooxie/article/details/108316723 、common_log.py 可以看 https://blog.csdn.net/zyooooxie/article/details/123635856
代码
@filename: common_function.py
"""
@blog: https://blog.csdn.net/zyooooxie
"""
import time
import os
import subprocess
import operator
import allure
import requests
import uiautomator2 as u2
from functools import wraps
from json import JSONDecodeError
from urllib.parse import urljoin
from requests_toolbelt.utils import dump
from typing import Union
from uiautomator2.exceptions import UiObjectNotFoundError
from uiautomator2.xpath import XPathSelector
from uiautomator2._selector import UiObject
from configparser import ConfigParser
from membership_interests_app_autotest.common_variable import screenshot_path, assert_all, find_selector
from membership_interests_app_autotest.common_variable import request_headers, result_path, report_path
from membership_interests_app_autotest.common_variable import install_app_path_dl, sn, xgtw_app_package
from membership_interests_app_autotest.common_variable import install_app_path_xgtw
from membership_interests_app_autotest.user_log import Log
def read_ini(ini_path: str = 'db.ini'):
"""
读取配置文件
:param ini_path:
:return:
"""
if os.path.isfile(ini_path):
config = ConfigParser()
config.read(ini_path, encoding='UTF-8')
return {cf: config.items(cf) for cf in config.sections()}
def kill_button(device: u2.Device, element_dict: dict, wait_time: int = None):
"""
app端的 升级-取消按钮、某些关闭弹窗
:param device:
:param element_dict:
:param wait_time:
:return:
"""
if wait_time is None:
wait_time = device.settings.get('wait_timeout')
try:
my_element(device, element_dict).click(timeout=wait_time)
except Exception as e:
Log.info('{}'.format(str(e)))
def my_screenshot(connect_addr: str):
"""
截图方法
:param connect_addr:
:return:
"""
img_name = ''.join([time.strftime("%Y%m%d_%H%M%S"), '.png'])
fn = os.path.join(screenshot_path, img_name)
ms_device = u2.connect(connect_addr)
ms_device.screenshot(filename=fn)
Log.error('已截图:{}'.format(img_name))
allure.attach.file(source=fn, name=img_name, attachment_type=allure.attachment_type.PNG)
def screenshot_decorator(connect_addr: str):
"""
截图装饰器
:param connect_addr:
:return:
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
func(*args, **kwargs)
except Exception as e:
Log.error(str(e))
my_screenshot(connect_addr)
raise e
return wrapper
return decorator
def my_element(device: u2.Device, element: Union[dict, str]):
"""
重新组装element
:param device:
:param element:
:return:
"""
Log.info(element)
if isinstance(element, str):
assert element.find('//') != -1
ele = device.xpath(element)
elif isinstance(element, dict):
check_selector(element)
ele = device(**element)
else:
raise Exception('Error Element:{}'.format(element))
return ele
def my_assert(device: u2.Device, element: Union[dict, str], functions: str = None,
assertion_content: Union[str, bool, tuple] = None,
assertion_condition: Union[str] = None, no_exists: bool = False, **kwargs):
"""
断言-assert_time
1。no_exists 传True: 直接断言元素exists
2。functions传 exists: functions='exists', assertion_content=True, assertion_condition='is_'
3。functions传 get_text: functions='get_text', assertion_content='立即升级', assertion_condition='eq'
4。functions传 其他,kwargs传参数:functions='center', assertion_content=(583.2, 976.2), assertion_condition='eq', offset=(0.6, 0.6)
assertion_condition 推荐使用 assert_all里面的
from membership_interests_app_autotest.common_variable import assert_all
:param device:
:param element:
:param functions:
:param assertion_content:
:param assertion_condition:
:param no_exists:
:param kwargs:
:return:
"""
if kwargs.get('assert_time') is None:
assert_time = device.settings.get('wait_timeout')
else:
assert_time = kwargs.get('assert_time')
kwargs.pop('assert_time')
# Log.debug(locals().get('assert_time'))
if assertion_condition is not None:
assert assertion_condition in assert_all
ele = my_element(device, element)
if no_exists:
assert ele.exists(timeout=assert_time) is False
print('断言元素不存在-{}'.format(assert_time), time.strftime('%H%M%S'))
else:
# 找不到元素,raise AssertionError
try:
if isinstance(ele, UiObject):
ele.must_wait(timeout=assert_time) # 等待
elif isinstance(ele, XPathSelector):
ele.wait(timeout=assert_time)
else:
Log.error(type(ele))
raise Exception('Element TypeError')
except UiObjectNotFoundError as e:
raise AssertionError(str(e))
if hasattr(ele, functions):
Log.debug('{}-{}'.format(ele, functions))
Log.debug('{}-{}'.format(assertion_condition, assertion_content))
if callable(getattr(ele, functions)):
Log.debug('{}-{}'.format(getattr(operator, assertion_condition), getattr(ele, functions)(**kwargs)))
# assertion_content 可以传 True、False、返回值(str)
assert getattr(operator, assertion_condition)(assertion_content, getattr(ele, functions)(**kwargs))
else:
assert getattr(operator, assertion_condition)(assertion_content, getattr(ele, functions))
else:
raise Exception('AttributeError: {} -- {}'.format(ele, functions))
def my_assert_toast(device: u2.Device, assert_message: str, assertion_condition: str,
element: Union[dict, str] = None, x=None, y=None, toast_text=None):
"""
对某元素、某元素的的坐标位置(百分比坐标)点击后,toast的断言
:param device:
:param assert_message:
:param assertion_condition:
:param element:触发toast的element
:param x:触发toast的元素 x坐标
:param y:触发toast的元素 y坐标
:param toast_text:toast的text内容,推荐只填写 ‘开头’
:return:
"""
assert element is not None or (x is not None and y is not None)
assert assertion_condition in assert_all
Log.info('assert_message:{}'.format(assert_message))
# toast uiautomator2 我就没试成功过,所以舍弃掉;
# while True:
#
# device.toast.reset()
#
# if element:
#
# my_element(device, element).click()
#
# else:
# Log.info('{}-{}'.format(x, y))
#
# device.double_click(x=x, y=y)
#
# message = device.toast.get_message(wait_timeout=0, cache_timeout=5)
# Log.info(message)
#
# if message:
# assert getattr(operator, assertion_condition)(message, assert_message)
#
# break
if element:
# 应该是要做断言,但 有时候确实找不到元素
# assert my_element(device, element).exists is True
Log.info(my_element(device, element).exists)
else:
assert x < 1 and y < 1
# 使用toast_text寻找toast_element
toast_element = dict(textStartsWith=toast_text)
im_time = device.settings.get('wait_timeout')
device.implicitly_wait(seconds=1)
while True: # 不限制执行次数
try:
if element:
my_element(device, element).click()
else:
Log.info('{}-{}'.format(x, y))
device.double_click(x=x, y=y)
real_text = my_element(device, toast_element).get_text()
Log.info('toast_element-{} 获取的text:{}'.format(toast_text, real_text))
except Exception as e:
Log.info(str(e))
real_text = None
if real_text:
assert getattr(operator, assertion_condition)(real_text, assert_message)
break
device.implicitly_wait(seconds=im_time)
def check_selector(element: dict):
"""
检查定位方式
:param element:
:return:
"""
find_selector_dict = dict.fromkeys(find_selector)
for e in element:
if e not in find_selector_dict:
raise UiObjectNotFoundError({'code': -32002, 'message': '__fields error', 'data': e}, method='定位方式没找到')
elif e in find_selector and e in ('index', 'instance'):
Log.info('定位方式: {} 不推荐使用'.format(e))
else:
pass
def clean_files(file_path: str):
"""
清理文件-想用于清理allure_result;
后来 使用pytest的命令--clean-alluredir 来清理
:param file_path:
:return:
"""
if os.path.isdir(file_path):
files = os.listdir(file_path)
for f in files:
os.remove(os.path.join(file_path, f))
else:
raise Exception('path error: {}'.format(file_path))
def create_allure():
"""
allure生成报告
:return:
"""
# -c --clean : Clean Allure report directory before generating a new one
# -o, --report-dir, --output The directory to generate Allure report into
cmd = 'allure generate {} -o {} -c'.format(result_path, report_path)
print(cmd)
res = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
encoding='gbk').communicate()[0]
print(res)
# # 推荐使用bat批文件
# if res.find('successfully'):
# os.system('allure open {}'.format(report_path))
def request_decorator(retry_times: int = 3, sleep_time: int = 3):
"""
请求装饰器
:param retry_times:重试次数
:param sleep_time:重试时 等待时间
:return:
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
if not res:
for rt in range(retry_times):
time.sleep(sleep_time)
res = func(*args, **kwargs)
if res:
return res
else:
Log.error('请留意:方法{},请求接口{},重试{}次,返回值一直异常'.format(func.__name__, kwargs.get('url'), retry_times))
raise Exception('Decorator Execute Error')
else:
return res
return wrapper
return decorator
@request_decorator()
def send_request(method: str, **kwargs):
"""
发送请求
:param method:
:param kwargs:
:return:
"""
assert hasattr(requests.api, method) is True
with getattr(requests, method)(**kwargs) as res:
try:
res_dict = res.json()
assert res_dict.get('success') is True
return res_dict
except (AssertionError, JSONDecodeError) as e:
Log.error('响应有问题-{}'.format(str(e)))
except Exception as e:
Log.error('请求失败-{}'.format(str(e)))
Log.error(dump.dump_all(res).decode('utf-8'))
return False
def uninstall_app(app_package: str, serial_number: str):
"""
adb卸载APP
:param app_package:
:param serial_number:
:return:
"""
Log.info('执行uninstall_app():{}'.format(app_package))
adb_cmd = 'adb -s {} uninstall {}'.format(serial_number, app_package)
Log.info(adb_cmd)
# os.system(command=adb_cmd)
r = os.popen(cmd=adb_cmd)
text = r.read()
Log.info(text)
r.close()
if not text.startswith('Success'):
# 卸载失败 不raise
Log.error('卸载失败')
def install_app(app_package: str, serial_number: str, app_path: str):
"""
adb安装APP
:param app_package:
:param serial_number:
:param app_path:
:return:
"""
uninstall_app(app_package, serial_number)
Log.info('执行install_app():{}'.format(app_path))
if app_path.endswith('.apk'):
# -g 为应用程序授予所有运行时的权限
# -r 替换已存在的应用程序
adb_cmd = 'adb -s {} install -g -r {}'.format(serial_number, app_path)
Log.info(adb_cmd)
try:
exit_code = subprocess.check_output(adb_cmd, shell=True)
except subprocess.CalledProcessError:
Log.info('the exit code was non-zero')
exit_code = None
else:
exit_code = exit_code.decode()
Log.info(exit_code)
if exit_code is None or exit_code.find('Success') == -1:
Log.error('安装失败')
raise EnvironmentError('安装失败')
if __name__ == '__main__':
pass
@filename: common_variable.py
"""
@blog: https://blog.csdn.net/zyooooxie
"""
import os
import random
import enum
from adbutils import adb
from membership_interests_app_autotest.user_log import Log
class AreaCode(enum.Enum):
XG = 852
TW = 886
dl_app_package = 'zyooooxie.activity'
dl_app_activity = 'zyooooxie.activity.MainActivity'
dl = [{'app_package': dl_app_package}]
dl_account_yy = '' # 白名单
dl_account_zc1 = ''
dl_account_zc2 = ''
# dl_account = random.choice([dl_account_yy, dl_account_zc1, dl_account_zc2])
dl_account = dl_account_zc1
xgtw_app_package = 'zyooooxie.new'
xgtw_app_activity = 'zyooooxie.new.MainActivity'
xg = [{'app_package': xgtw_app_package, 'area_code': AreaCode.XG}]
tw = [{'app_package': xgtw_app_package, 'area_code': AreaCode.TW}]
# xg_account = random.choice(['', '', ''])
xg_account = '' # 可以支付
tw_account = random.choice(['', '', ''])
current_path = os.path.abspath(__file__)
parent_path = os.path.dirname(current_path)
result_path = os.path.join(parent_path, 'allure_result')
report_path = os.path.join(parent_path, 'allure_report')
screenshot_path = os.path.join(parent_path, 'screenshot_folders') # 用例执行错误时,用到的
# 图像对比时 用到的
pictures_path = os.path.join(parent_path, 'picture_folders')
now_pictures_path = os.path.join(pictures_path, 'picture_now')
diff_pictures_path = os.path.join(pictures_path, 'picture_diff')
sn = '' # 常用设备
# print(adb.device(sn).get_state()) # device、device not found
common_serial_number = random.choice(adb.device_list()).serial if sn not in [d.serial for d in
adb.device_list()] else sn
request_headers = {'version': 'https://blog.csdn.net/zyooooxie'}
install_app_path_dl = r''
install_app_path_xgtw = r''
# uiautomator2._selector,Selector的 __fields
find_selector = ['text', 'textContains', 'textMatches', 'textStartsWith', 'className', 'classNameMatches',
'description', 'descriptionContains', 'descriptionMatches', 'descriptionStartsWith',
'resourceId', 'resourceIdMatches', 'index', 'instance']
# operator 的 __all__
assert_all = ['abs', 'add', 'and_', 'attrgetter', 'concat', 'contains', 'countOf',
'delitem', 'eq', 'floordiv', 'ge', 'getitem', 'gt', 'iadd', 'iand',
'iconcat', 'ifloordiv', 'ilshift', 'imatmul', 'imod', 'imul',
'index', 'indexOf', 'inv', 'invert', 'ior', 'ipow', 'irshift',
'is_', 'is_not', 'isub', 'itemgetter', 'itruediv', 'ixor', 'le',
'length_hint', 'lshift', 'lt', 'matmul', 'methodcaller', 'mod',
'mul', 'ne', 'neg', 'not_', 'or_', 'pos', 'pow', 'rshift',
'setitem', 'sub', 'truediv', 'truth', 'xor']
Log.info('{}-{}-{}'.format(xg_account, tw_account, dl_account))
Log.info(common_serial_number)
if __name__ == '__main__':
pass
本文链接:https://blog.csdn.net/zyooooxie/article/details/123916767
交流技术 欢迎+QQ 153132336 zy
个人博客 https://blog.csdn.net/zyooooxie