九、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/  这位博主的文章很不错,很适合入门。

原文地址:https://www.cnblogs.com/Cc905/p/13837056.html