• 九、appium自动化框架综合实践


      结合前面的元素寻找、操作、unittest测试框架,搭建一个完整的自动化框架。本篇旨在框架设计、单机用例执行、输出报告,下篇继续实践Bat批处理执行测试、多设备并发测试。

    框架功能

    • 数据配置
    • 日志输出
    • 截图处理
    • 基础功能封装(公共方法,查找元素)
    • 业务功能
    • 数据驱动
    • 测试用例封装
    • 断言处理和报告输出

    测试需求

    测试环境

    • win10
    • appium 1.17.1
    • weixin
    • 真机

    测试用例

    登录场景1.用户名 xxx 密码 xxx    (登录成功)

    登录场景2.用户名 xxx 密码 xxx    (登录失败)

    框架设计

     

    代码实现

    1.日志输出

    # -*- coding:utf-8 -*-
    # __author__ = "Cc"
    
    import logging
    import time
    
    
    class OutputLog:
        critical = logging.CRITICAL  # 级别最高,什么也不输出
        fatal = logging.FATAL
        error = logging.ERROR
        warning = logging.WARNING
        info = logging.INFO
        debug = logging.DEBUG
        @classmethod
        def output_log(cls, log_level=debug):
            my_logging = logging.getLogger(__name__)
            my_logging.setLevel(log_level)
            if not my_logging.handlers:
                local_time = time.localtime()
                file_name1 = time.strftime('%Y-%m-%d', local_time)
                file_name2 = r"Logging\"
                file_name = file_name2 + file_name1 + ".log"
                file_handler = logging.FileHandler(file_name, "a", encoding="utf-8")  # 输出日志到磁盘文件
                file_handler.setLevel(log_level)
                formatter = logging.Formatter("%(asctime)s--%(levelname)s--%(process)d--"
                                            "%(thread)d--%(threadName)s--%(funcName)s--%(lineno)d--%(lineno)d : %(message)s")
                file_handler.setFormatter(formatter)
                my_logging.addHandler(file_handler)
            return my_logging

    遇到的问题和解决方法

    举个调用的例子:

    OutputLog.output_log().debug("==============开始测试,连接手机==============")
    OutputLog.output_log().debug("==============第二次调用==============")

    上面代码,第一行日志输出只输出了一次,第二行输出了两次,原因是我在一开始实现时,每次调用都会重新创建一个handles,使用完后没有删除,同一log对象有多个handles,日志会重复输出,所以我在创建handles前先加以判断:if not my_logging.handlers,如果存在则不重新创建了。

    2.设备初始化

    2.1设备信息

    保存设备信息在devices.yaml中,可以通过修改此文件,修改设备信息。

    oppo_findx_pro:
      appActivity: com.tencent.mm.ui.LauncherUI
      appPackage: com.tencent.mm
      autoGrantPermissions: true
      automationName: UiAutomator2
      chromeOptions:
        androidProcess: com.tencent.mm:toolsmp
      chromedriverExecutable: C:Usersv_yddchenDesktopchromedriver_win32 77.0chromedriver.exe
      deviceName: dd
      noReset: false
      platFormVersion: 10
      platformName: Android
      resetKeyboard: true
      udid: 648d4f29
      unicodeKeyboard: true
    oppo_reno:
      appActivity: com.tencent.mm.ui.LauncherUI
      appPackage: com.tencent.mm
      autoGrantPermissions: true
      automationName: UiAutomator2
      chromeOptions:
        androidProcess: com.tencent.mm:toolsmp
      chromedriverExecutable: C:Usersv_yddchenDesktopchromedriver_win32 77.0chromedriver.exe
      deviceName: df93a63a
      noReset: false
      platFormVersion: 9
      platformName: Android
      resetKeyboard: true
      udid: df93a63a
      unicodeKeyboard: true

     

    2.2 初始化

    初始化操作

    # 初始化设备
    
    # -*- coding:utf-8 -*-
    # __author__ = "Cc"
    
    from appium import webdriver
    import yaml
    from OutputLog import OutputLog
    from login import Login
    import time
    
    
    class InitDevices:
        def __init__(self, file_name, device_name):
            self.file_name = file_name
            self.device_name = device_name
    
        def read_devices(self):
            """
            获取设备信息
            :return:
            """
            try:
                OutputLog.output_log().debug("尝试获取设备信息")
                with open(self.file_name, 'r', encoding='utf-8') as f:
                    all_devices = yaml.safe_load(f.read())
            except IOError:
                OutputLog.output_log().error("设备文件读取错误")
            else:
                msg = str(all_devices[self.device_name])
                OutputLog.output_log().debug(msg)
                return all_devices[self.device_name]
    
        def init_devices(self, device_info):
            """
            初始化设备
            :param device_info:
            :return:
            """
            return webdriver.Remote("http://localhost:4723/wd/hub", device_info)
    
    
    if __name__ == "__main__":
        OutputLog.output_log().debug("==============开始测试,连接手机==============")
        devices_object = InitDevices('devices.yaml', 'oppo_findx_pro')
        devices_info = devices_object.read_devices()
        devices = devices_object.init_devices(devices_info)
        OutputLog.output_log().debug("连接成功")  # 连接成功,开始找元素
        file_name = 'screenshots/' + '测试' + '.png'
        devices.find_element_by_android_uiautomator('new UiSelector().textMatches("(.*)录")')
        devices.get_screenshot_as_file(file_name)
        devices.implicitly_wait(5)
        login_els = Login(devices)
        time.sleep(2)
        time.sleep(2)

    3.获取测试数据

    3.1 数据准备

    3.2 读取数据

    # 读取测试数据
    # -*- coding:utf-8 -*-
    # __author__ = "Cc"
    
    import csv
    
    
    class ReadData:
        def __init__(self, file_name):
            self.file_name = file_name
    
        def read_data(self):
            """
            注意文件不能有中文,否则会报错
            :return: 二维数组data
            """
            with open(self.file_name, 'r', encoding='utf-8') as f:
                csv_reader = csv.reader(f)
                head = next(csv_reader)
                # print(head)
                data = [[]]
                if len(data):
                    data.clear()  # 如果没有这一步,data会存在一个空值
                    for data1 in csv_reader:
                        data.append(data1)
                else:
                    for data1 in csv_reader:
                        data.append(data1)
                # print(data)
                return data
    
    
    if __name__ == "__main__":
        re = ReadData("login_msg.csv")
        re.read_data()

    4.公共方法

    寻找元素的公共方法的封装

    # 基类,查找元素
    
    # -*- coding:utf-8 -*-
    # __author__ = "Cc"
    
    from appium.webdriver import webdriver
    from selenium.webdriver.common.by import By
    from OutputLog import OutputLog
    
    
    class BaseFindEl:
        def __init__(self, devices):
            """
            传入设备
            :param devices:
            """
            self.devices = devices
    
        def find_el_by_text(self, **kw):
            """
            根据传入text时关键字参数的名称,决定调用text的哪一个方法
            :param kw: 查找元素的text
            :return: 返回找到的元素
            """
            if 'text' in kw:
                text = kw['text']
                path = 'new UiSelector().text("{}")'.format(text)
                return self.devices.find_element_by_android_uiautomator(path)
            elif 'textContains' in kw:
                text = kw['textContains']
                path = 'new UiSelector().textContains("{}")'.format(text)
                return self.devices.find_element_by_android_uiautomator(path)
            elif 'textStarsWith' in kw:
                text = kw['textStarsWith']
                path = 'new UiSelector().textStarsWith("{}")'.format(text)
                return self.devices.find_element_by_android_uiautomator(path)
            elif 'textMatches' in kw:
                text = kw['textMatches']
                path = 'new UiSelector().textMatches("{}")'.format(text)
                return self.devices.find_element_by_android_uiautomator(path)
            else:
                OutputLog.output_log().error("没有匹配到查找方法")
    
        def find_el_by_class_name(self, **kw):
            """
            根据传入的className查找元素
            :param kw: className
            :return: 找到的元素
            """
            if 'className' in kw:
                text = kw['className']
                path = 'new UiSelector().className("{}")'.format(text)
                return self.devices.find_element_by_android_uiautomator(path)
            elif 'classNameContains' in kw:
                text = kw['classNameContains']
                path = 'new UiSelector().classNameContains("{}")'.format(text)
                return self.devices.find_element_by_android_uiautomator(path)
            else:
                OutputLog.output_log().error("没有匹配到查找方法")
    
        def find_el_by_resource_id(self, **kw):
            """
            根据传入的resourceId查找元素
            :param kw:
            :return:
            """
            if 'resourceId' in kw:
                text = kw['resourceId']
                path = 'new UiSelector().resourceId("{}")'.format(text)
                return self.devices.find_element_by_android_uiautomator(path)
            elif 'resourceIdMatches' in kw:
                text = kw['resourceIdMatches']
                path = 'new UiSelector().resourceIdMatches("{}")'.format(text)
                return self.devices.find_element_by_android_uiautomator(path)
            else:
                OutputLog.output_log().error("没有匹配到查找方法")
    
        def find_el_by_multi_values(self, **values):
            """
            组合多个属性
            :param values:
            :return:
            """
            pass

    5.业务功能

    # 查找登录界面所有的元素
    
    # -*- coding:utf-8 -*-
    # __author__ = "Cc"
    
    from BaseFindEl import BaseFindEl
    from selenium.common.exceptions import NoSuchElementException
    from selenium.common.exceptions import TimeoutException
    from selenium.webdriver.support.ui import WebDriverWait
    from OutputLog import OutputLog
    
    
    class Login(BaseFindEl):
        def __init__(self, devices):
            BaseFindEl.__init__(self, devices)
    
        def login(self, text="登录"):
            """
            点击登录
            :param text:
            :return:
            """
            self.devices.implicitly_wait(3)
            try:
                btn = self.find_el_by_text(text=text)
            except NoSuchElementException:
                msg = "错误:没有找到{}控件".format(text)
                OutputLog.output_log().error(msg)
            else:
                OutputLog.output_log().debug("进入到登录界面")
                btn.click()
    
        def sign_in(self, text="注册"):
            """
            :param text: 注册
            :return:
            """
            self.devices.implicitly_wait(3)
            try:
                btn = self.find_el_by_text(text=text)
            except NoSuchElementException:
                msg = "错误:没有找到{}控件".format(text)
                OutputLog.output_log().error(msg)
            else:
                OutputLog.output_log().debug("进入到注册界面")
                btn.click()
    
        def switch_to_username(self, text_contains="用微信号"):
            """
            :param text_contains: 切换到微信号/QQ号登录
            :return:
            """
            try:
                btn = self.find_el_by_text(textContains=text_contains)
            except NoSuchElementException:
                msg = "错误:没有找到{}控件".format(text_contains)
                OutputLog.output_log().error(msg)
            else:
                OutputLog.output_log().debug("切换到微信号输入界面")
                btn.click()
    
        def user_edit(self, text_contains="请填写微信号"):
            """
            寻找账号输入框
            :param text_contains:
            :return: 账号阿输入edit
            """
            try:
                username_edit = self.find_el_by_text(textContains=text_contains)
            except NoSuchElementException:
                msg = "错误:没有找到{}控件".format(text_contains)
                OutputLog.output_log().error(msg)
            else:
                OutputLog.output_log().debug("找到了账号输入编辑框")
                return username_edit
    
        def pwd_edit(self, text_contains="请填写密码"):
            """
            寻找账号输入框
            :param text_contains:
            :return: 账号阿输入edit
            """
            try:
                pwd_edit = self.find_el_by_text(textContains=text_contains)
            except NoSuchElementException:
                msg = "错误:没有找到{}控件".format(text_contains)
                OutputLog.output_log().error(msg)
            else:
                OutputLog.output_log().debug("找到了账号输入编辑框")
                return pwd_edit
    
        def input_msg(self, user_name, pwd):
            """
            输入信息
            :param user_name: 账号名称
            :param pwd: 密码
            :return:
            """
            name_edit = self.user_edit()
            name_edit.clear()
            pwd_edit = self.pwd_edit()
            pwd_edit.clear()
            name_edit.send_keys(user_name)
            pwd_edit.send_keys(pwd)
    
        def find_toast(self, text="正在"):
            text_1 = "//*[contains(@text,'{}')]".format(text)
            toast = WebDriverWait(self.devices, 5, 0.00000001).until(lambda x: x.find_element_by_xpath(text_1))
            return toast.text
    
        def login_fail(self, screenshots_nam):
            """
            处理登录失败的弹窗
            :param screenshots_nam: 截图保存的名称
            :return:
            """
            try:
                WebDriverWait(self.devices, 3).
                    until(lambda x: x.find_element_by_android_uiautomator('new UiSelector().textContains("密码错误")'))
            except TimeoutException:
                OutputLog.output_log().error("测试失败,没有找到登录失败的弹窗")
                file_name = 'screenshots/' + screenshots_nam + '.png'
                self.devices.get_screenshot_as_file(file_name)
                return 0
            else:
                OutputLog.output_log().debug("出现登录失败的弹窗")
                file_name = 'screenshots/' + screenshots_nam + '.png'
                self.devices.get_screenshot_as_file(file_name)
                self.find_el_by_text(text='确定').click()
                return 1
    
        def authorization_actions(self, screenshots_nam):
            try:
                WebDriverWait(self.devices, 3). 
                    until(lambda x: x.find_element_by_android_uiautomator('new UiSelector().textMatches("(.*)权限申请")'))
            except TimeoutException:
                OutputLog.output_log().debug("没有出现权限申请弹窗")
                file_name = 'screenshots/' + screenshots_nam + '.png'
                self.devices.get_screenshot_as_file(file_name)
            else:
                OutputLog.output_log().debug("权限申请提示")
                file_name = 'screenshots/' + screenshots_nam + '.png'
                self.devices.get_screenshot_as_file(file_name)
                self.find_el_by_text(text='我知道了').click()
    
        def phone_authorization_actions(self, screenshots_nam, action='允许'):
            """
    
            :param screenshots_nam: 保存的截图
            :param action: 允许或者拒绝
            :return:
            """
            try:
                WebDriverWait(self.devices, 4). 
                    until(lambda x: x.find_element_by_android_uiautomator('new UiSelector().textMatches("(.*)电话权限")'))
            except TimeoutException:
                OutputLog.output_log().debug("没有出现电话权限申请弹窗")
                file_name = 'screenshots/' + screenshots_nam + '.png'
                self.devices.get_screenshot_as_file(file_name)
            else:
                OutputLog.output_log().debug("电话权限申请提示")
                file_name = 'screenshots/' + screenshots_nam + '.png'
                self.devices.get_screenshot_as_file(file_name)
                self.find_el_by_text(text=action).click()
    
        def sd_card_authorization_actions(self, screenshots_nam, action='允许'):
            """
    
            :param screenshots_nam: 保存的截图
            :param action: 允许或者拒绝
            :return:
            """
            try:
                WebDriverWait(self.devices, 4). 
                    until(lambda x: x.find_element_by_android_uiautomator('new UiSelector().textMatches("(.*)空间权限")'))
            except TimeoutException:
                OutputLog.output_log().debug("没有出现空间权限申请弹窗")
                file_name = 'screenshots/' + screenshots_nam + '.png'
                self.devices.get_screenshot_as_file(file_name)
            else:
                OutputLog.output_log().debug("空间权限申请提示")
                file_name = 'screenshots/' + screenshots_nam + '.png'
                self.devices.get_screenshot_as_file(file_name)
                self.find_el_by_text(text=action).click()

    6.用例执行和报告输出

    # 执行用例
    # -*- coding:utf-8 -*-
    # __author__ = "Cc"
    
    from InitDevices import InitDevices
    from OutputLog import OutputLog
    from read_msg import ReadData
    from login import Login
    
    import unittest
    import time
    import os
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.common.exceptions import NoSuchElementException
    from selenium.common.exceptions import TimeoutException
    import HTMLReport
    
    
    class LoginUnittest(unittest.TestCase):
        devices_object = None
        devices = None
        data = None
        index0 = 0
        login_object = None
    
        def __init__(self, *args, **kwargs):
            """每个用例执行前,__init__都会执行一次"""
            super().__init__(*args, **kwargs)
    
        @classmethod
        def setUpClass(cls):
            """
            初始化设备,读取测试数据,获取一个测试对象
            :return:
            """
            OutputLog.output_log().debug("==============开始测试,连接手机==============")
            cls.devices_object = InitDevices('devices.yaml', 'oppo_findx_pro')
            devices_info = cls.devices_object.read_devices()
            cls.devices = cls.devices_object.init_devices(devices_info)  # 返回设备对象
            OutputLog.output_log().debug("连接成功")  # 连接成功,开始操作
            cls.data = ReadData("login_msg.csv").read_data()  # 获取登录数据
            cls.index0 = 0
            cls.login_object = Login(cls.devices)
    
        @classmethod
        def tearDownClass(cls):
            """
            devices.quit()
            :return:
            """
            # OutputLog.output_log().debug("测试结束")
            # cls.devices.quit()
            f = os.popen(r"adb shell dumpsys activity top | findstr ACTIVITY", "r")  # 获取当前界面的Activity
            current_activity = f.read()
            f.close()
            print(current_activity)  # cmd输出结果
    
            # 用in方法 判断一个字符串是否包含某字符
            appackage_name = 'com.ximalaya.ting.android'
            if appackage_name in current_activity:
                cls.drivers.quit()
            else:
                pass
    
        def setUp(self):
            """
            每个用例执行前执行,这里切换登录方式
            :return:
            """
            LoginUnittest.login_object.login()
            time.sleep(1)
            LoginUnittest.login_object.switch_to_username()
    
        def tearDown(self):
            """
            每个用例执行后执行,os.system("adb shell pm clear com.tencent.mm"),执行成功返回0
            :return:
            """
            time.sleep(1)
            if not os.system("adb shell pm clear com.tencent.mm"):
                # os.system("adb shell pm grant com.tencent.mm")
                OutputLog.output_log().debug("清除应用数据")
                LoginUnittest.index0 = LoginUnittest.index0 + 1
                time.sleep(3)
                # LoginUnittest.devices.start_activity('com.tencent.mm', '.ui.LauncherUI')
                os.system('adb shell am start com.tencent.mm/.ui.LauncherUI')
            else:
                OutputLog.output_log().debug("清除应用数据失败")
                time.sleep(2)
    
        # 测试登录失败
        def test_login_1(self):
            user_name = LoginUnittest.data[LoginUnittest.index0][0]
            pwd = LoginUnittest.data[LoginUnittest.index0][1]
            LoginUnittest.login_object.input_msg(user_name, pwd)
            msg = "登录信息" + user_name + pwd
            OutputLog.output_log().debug(msg)
            LoginUnittest.login_object.login()  # 登录
            # try:
            #     btn = WebDriverWait(LoginUnittest.devices, 7).
            #         until(lambda x: x.find_element_by_android_uiautomator('new UiSelector().text("通讯录")'))
            # except TimeoutException:
            #     OutputLog.output_log().debug('登录失败')
            #     self.assertEqual(1, 1, '登录失败')
            file_name = "test_login_1" + "登录失败"
            result = LoginUnittest.login_object.login_fail(file_name)
            self.assertEqual(result, 1, '失败')
    
        def test_login_2(self):
            user_name = LoginUnittest.data[LoginUnittest.index0][0]
            pwd = LoginUnittest.data[LoginUnittest.index0][1]
            LoginUnittest.login_object.input_msg(user_name, pwd)
            msg = "登录信息" + user_name + pwd
            OutputLog.output_log().debug(msg)
            LoginUnittest.login_object.login()
            file_name_1 = 'test_login_2' + '权限申请提醒'
            LoginUnittest.login_object.authorization_actions(file_name_1)
            file_name_3 = 'test_login_2' + '存储权限申请提醒'
            LoginUnittest.login_object.phone_authorization_actions(file_name_3)
            file_name_2 = 'test_login_2' + '电话权限申请提醒'
            LoginUnittest.login_object.phone_authorization_actions(file_name_2)
            try:
                btn = WebDriverWait(LoginUnittest.devices, 7).
                    until(lambda x: x.find_element_by_android_uiautomator('new UiSelector().text("通讯录")'))
            except TimeoutException:
                OutputLog.output_log().debug('登录失败')
            else:
                self.assertEqual(btn.text, '通讯录', '测试成功')
    
    
    if __name__ == '__main__':
        test_suite = unittest.TestSuite()
        tests = [LoginUnittest('test_login_1'), LoginUnittest('test_login_2')]
        test_suite.addTests(tests)
        # runner = unittest.TextTestRunner()
        runner = HTMLReport.TestRunner(
            report_file_name="login_reports",
            output_path="login_report",
            title="登录功能测试报告",
            description="测试登录功能",
            thread_count=1,
            thread_start_wait=0,
            tries=0,
            delay=0,
            back_off=1,
            retry=False,
            sequential_execution=True,
            lang="cn"
    
        )
        runner.run(test_suite)

    7.执行结果

    HTMLReport默认会输出一份.html报告文件和一份日志文件。只执行了两个用例,但是耗时0:2:28,代码还有待优化,Bat批处理命令执行,会不会对用例的执行效率有所提升呢?期待接下来的实践。

     参考链接:https://sutune.me/2018/05/10/appium-autoTest-frame/  这位博主的文章很不错,很适合入门。

  • 相关阅读:
    Windows Service 2016 DatacenterStandEmbedded激活方法
    批处理文件设置IP以及DNS
    C#类的一些基础知识(静态方法可以不用实例化调用)
    Dynamics Crm Plugin插件注册的问题及解决方案(持续更新。。。。。。)
    【转载】C# get 与set的一些说明
    C#补位函数PadLeft和PadRight
    Kubernetes集群调度之Scheduler
    Kubernetes集群控制之ControllerManager
    Kubernetes集群大脑之apiserver
    Kubernetes集群存储之etcd
  • 原文地址:https://www.cnblogs.com/Cc905/p/13837056.html
Copyright © 2020-2023  润新知