uiautomator2 App自动化测试框架【一】

本文为博主原创,未经授权,严禁转载及使用。
本文链接: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

需求

  1. 使用uiautomator2;
  2. 用例是有 2个app + 手机浏览器访问某些H5页面,总条数在30条左右;

2个APP:其中一个是内地版,另一个是港澳台定制版。

  1. 港澳台定制版app 要使用不同账号登录、退出;

港澳台定制版 主要验证的地区是 香港、台湾。

  1. 用例的前、后置要用 pytest的fixture + 普通setup和teardown;
  2. 2个app都要执行 新的卸载、安装;

框架设计

使用:uiautomator2+ pytest + allure + pymysql + requests

按理说,pymysql + requests 是用不上的,但实际用例执行前的预置、执行后数据清理 要用到的。

  1. 清理本地文件(截图),日志不清理;
  2. 安装App、用户登录 or 浏览器打开URL、用户登录;
  3. po模式,执行用例、 断言;
  4. 退出登录、卸载App;
  5. 生成测试报告【非必要】;

因为生成报告、定时执行 是在大佬的平台上来做,所以我需要做的主要是 前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

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值