Android测试框架改造 (数据步骤驱动 测试数据驱动 自动化异常处理机制 日志添加 错误截图 录屏功能)

测试框架思想

为什么要使用框架

业务场景复杂
录制/回放无法适应复杂场景
自动化脚本工作量大且可维护性差
PageObject结构松散,无法在多项目中迁移

设计思想

1、PageObject设计模式对UI机测试进行封装
2、PO改进
数据驱动,异常处理
3、Pytest 单元测试
改进方向
1、测试数据的数据驱动
2、数据步骤的数据驱动
3、自动化异常处理机制

PageObject改造

目录结构
在这里插入图片描述

一、基本封装

在这里插入图片描述
app.py

from appium import webdriver
from page.base_page import BasePage
from page.main import Main

class App(BasePage):
    _package = "com.xueqiu.android"
    _activity = ".view.WelcomeActivityAlias"

    def start(self):
        if self._driver is None:
            desire_cap= dict()
            desire_cap["platformName"]= "android"
            desire_cap["deviceName"]= "emulator-5554"
            desire_cap["appPackage"]= self._package
            desire_cap["appActivity"]=self._activity
            self._driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub',desire_cap)
        else:
            self._driver.start_activity(self._activity,self._package)
        self._driver.implicitly_wait(15)
        return self

    def main(self):
        return Main(self._driver)

base_page.py

from appium.webdriver.webdriver import WebDriver

class BasePage:
    _driver: WebDriver
    def __init__(self,driver:WebDriver=None):
        self._driver = driver

    def find(self,locator,value):
        return self._driver.find_element(locator,value)

main.py

from selenium.webdriver.common.by import By
from page.base_page import BasePage

class Main(BasePage):
    def goto_search(self):
        self.find(By.ID,'tv_search').click()

test_main.py

from page.app import App
import pytest

class TestMain:
    def test_main(self):
        app = App()
        app.start().main().goto_search()

if __name__ == '__main__':
    pytest.main()
二、测试步骤数据驱动封装优化(案例:利用yaml编写测试用例,其他衍生可以使用Excel,数据库,Json等)

意义:1、提高测试代码的编写效率2、异常排查效率高3、代码可维护高
Yaml格式
python:安装PyYaml
Yaml
by:id
locator:tv_search
action:click
重点读取yaml关键的操作:

        with open(path) as f:
            steps = yaml.safe_load(f)
        print(steps)
        element = None
        for step in steps:
            if "by" in step.keys():
                element = self.find(step["by"],step["locator"])
            if "action" in step.keys():
                action = step["action"]
                if action == "click":
                    element.click()

整体文件目录
在这里插入图片描述
main.yaml 注意yaml书写,不然读不出对应的格式

- by: xpath
  locator: '//*[@text="行情"]'
  action: click
- by: xpath
  locator: 'v_search'
  action: 'send_keys'
  value: '阿里巴巴'

app.py

from appium import webdriver
from page.base_page import BasePage
from page.main import Main

class App(BasePage):
    _package = "com.xueqiu.android"
    _activity = ".view.WelcomeActivityAlias"

    def start(self):
        if self._driver is None:
            desire_cap= dict()
            desire_cap["platformName"]= "android"
            desire_cap["deviceName"]= "emulator-5554"
            desire_cap["appPackage"]= self._package
            desire_cap["appActivity"]=self._activity
            self._driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub',desire_cap)
        else:
            self._driver.start_activity(self._activity,self._package)
        self._driver.implicitly_wait(15)
        return self

    def main(self):
        return Main(self._driver)

base_page.py

import yaml
from appium.webdriver.webdriver import WebDriver

class BasePage:
    _driver: WebDriver

    def __init__(self,driver:WebDriver=None):
        self._driver = driver

    def find(self,locator,value):
        return self._driver.find_element(locator,value)

    def steps(self,path):
        with open(path) as f:
            steps = yaml.safe_load(f)
        print(steps)
        element = None
        for step in steps:
            if "by" in step.keys():
                element = self.find(step["by"],step["locator"])
            if "action" in step.keys():
                action = step["action"]
                if action == "click":
                    element.click()
                 if 'send_key' == step['action']:
                     self.element.send_keys(step['value'])

main.py

from selenium.webdriver.common.by import By
from page.base_page import BasePage

class Main(BasePage):
    def goto_search(self):
        # self.find(By.ID,'tv_search').click()
        self.steps("../page/main.yaml")

test_main.py

from page.app import App
import pytest

class TestMain:
    def test_main(self):
        app = App()
        app.start().main().goto_search()

if __name__ == '__main__':
    pytest.main()
三、测试数据的数据驱动

思想
1、对部分数据进行参数化
2、把参数化的数据放入yaml,json等文件
意义
1、提高数据的维护性
2、让数据变得规范
3、可对数据备份
在这里插入图片描述
test_main.yaml

-
  aaa
  bbb
-
  ccc
  ddd

test_main.py

import yaml
from page.app import App
import pytest

class TestMain:
    @pytest.mark.parametrize("v1,v2",yaml.safe_load(open("../test_case/test_main.yaml")))
    def test_main(self,v1,v2):
        # app = App()
        # app.start().main().goto_search()
        print(v1)
        print(v2)

if __name__ == '__main__':
    pytest.main()
配置的数据驱动

思想
抽离公共数据
数据存入yaml中
configuration.yaml

caps:
  udid: emulator-5554

app.py

    def start(self):
        if self._driver is None:
            desire_cap= dict()
            desire_cap["platformName"]= "android"
            desire_cap["deviceName"]= "emulator-5554"
            desire_cap["appPackage"]= self._package
            desire_cap["appActivity"]=self._activity
            desire_cap["udid"]= yaml.safe_load(open("../page/configuration.yaml"))['caps']['udid']
            self._driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub',desire_cap)
        else:
            self._driver.start_activity(self._activity,self._package)
        self._driver.implicitly_wait(15)
        return self
通用Page封装

思想
1、将公共操作放入page_object
2、Page_object只放公共操作

自动化异常处理机制

弹窗问题
1、弹窗请求
2、弹窗广告
3、弹窗好评…
导致的问题
1、阻碍测试用例的正常执行
2、测试用例卡在弹窗页面
解决思路
1、利用try catch捕获异常
2、异常处理弹窗
处理步骤:在有可能出现弹窗的地方加一个关闭的步骤,如果有就关,没有直接走try的逻辑

    def find(self,locator,value):
        black_list = [(By.ID, "iv_close")]  # 黑名单
        try:
            element = self._driver.find_element(locator,value)
            return element
        except:
            for black in black_list:
                elements = self._driver.find_elements(*black)
                if len(elements) > 0:
                    elements[0].click()
                    break
            return self._driver.find_element(locator,value)
通用测试用例封装

一、解决对于高频重复调用的用例
解决思路
使用TestBase封装通用的测试用例
使用Fixture

装饰器

场景使用装饰器处理自定义find方法中的黑名单,日志等
base_page.py

import yaml
from appium.webdriver.webdriver import WebDriver
from appium_xueqiu.page.wrapper import handle_black

class BasePage:
    _driver: WebDriver

    def __init__(self,driver:WebDriver=None):
        self._driver = driver

    def finds(self,locator,value):
        if isinstance(locator,tuple):
           elements = self._driver.find_elements(*locator)
        else:
           elements = self._driver.find_element(locator,value)
        return elements

    # def find(self,locator,value):
    #     # return self._driver.find_element(locator, value)
    #     black_list = [('By.XPATH','//*[@text="行情"]')]
    #     try:
    #         print("进入")
    #         element = self._driver.find_element(locator,value)
    #         return element
    #     except:
    #         print("进入错误")
    #         for black in black_list:
    #             elements = self._driver.find_elements(*black)
    #             if len(elements) > 0:
    #                 elements[0].click()
    #                 break
    #         return self._driver.find_element(locator,value)

    @handle_black
    def find(self,locator,value:str=None):  # 默认value值为None,当locator是元组tuple时,不传
        if isinstance(locator,tuple):       # 判断locator是否是元组
            ele = self._driver.find_element(*locator)  # 如果是解构获取值
        else:
            ele = self._driver.find_element(locator,value)
        return ele

装饰器wrapper.py

import logging
from selenium.webdriver.common.by import By

def handle_black(func):
    def wrapper(*args,**kwargs):
        from appium_xueqiu.page.base_page import BasePage
        logging.basicConfig(level=logging.INFO)
        black_list = [(By.XPATH,'//*[@text="行情"]')]  # 黑名单
        instance: BasePage = args[0] # 这里的args[0]是被调方法的第一是参数,比如这里我们要在find方法上添加这个方法,那这个instance就是find的self,指的对象就是BasePage
        try:
            element = func(*args,**kwargs)  # 这里执行装饰器要用方法
            return element
        except Exception as e:
            print(e)
            for black in black_list:
                logging.info(black)
                ele_list = instance._driver.find_elements(*black)
                if len(ele_list) > 0:
                    ele_list[0].click()
                    # 点击掉黑名单后,再次寻找
                    return wrapper(*args,**kwargs)
    return wrapper
通用测试用例步骤封装

场景:当多个操作时,把所有的步骤都放到一个文件中执行(这里注意yaml文件的两个字典的格式)

hangqing:
  - by: xpath
    locator: '//*[@text="行情"]'
    action: click

search:
  - by: xpath
    locator: 'v_search'
    action: 'send_keys'
    value: '阿里巴巴'

base_page.py中step方法改造

    def steps(self,path,name):
        with open(path) as f:
            steps = yaml.safe_load(f)[name] # yaml.safe_load(f)取出字典格式->然后list->字典获取对应的值
        print(steps)
        element = None
        for step in steps:
            if "by" in step.keys():
                element = self.find(step["by"],step["locator"])
            if "action" in step.keys():
                action = step["action"]
                if action == "click":
                    element.click()

上的改造需要name的变量填写,这里我们使用inspect.stack()方法,来进一步优化

    def steps(self,path):
        with open(path) as f:
        	name = inspect.stack()[1].function
            steps = yaml.safe_load(f)[name] # yaml.safe_load(f)取出字典格式->然后list->字典获取对应的值
        print(steps)
        element = None
        for step in steps:
            if "by" in step.keys():
                element = self.find(step["by"],step["locator"])
            if "action" in step.keys():
                action = step["action"]
                if action == "click":
                    element.click()
yaml文件中数据驱动(变量替换)

basepage.py

    def steps(self,path,name):
        self._param = {}  # 存储字典
        with open(path) as f:
            steps = yaml.safe_load(f)[name] # yaml.safe_load(f)取出字典格式->然后list->字典获取对应的值
        raw = json.dumps(steps)  # 将字典转成为string
        for key,value in self._param.items():   # 将字典_param的key和value遍历出来
            raw.replace(f'${{{key}}}',value)   # 替换raw字符串中的对应的key

        element = None
        for step in steps:
            if "by" in step.keys():
                element = self.find(step["by"],step["locator"])
            if "action" in step.keys():
                action = step["action"]
                if action == "click":
                    element.click()

test_search.py

class Search(BasePage):
    @pytest.mark.parametrize("name",yaml.safe_load(open("test_search.yaml",encoding="utf-8")))
    def search(self, name):
    	self.search.search(name)

test_search.yaml

-"alibaba"
-"阿里巴巴"
日志添加

日志基础说明:
首先引入了 logging 模块,然后进行了一下基本的配置,这里通过 basicConfig 配置了 level 信息和 format 信息,这里 level 配置为 INFO 信息,即只输出 INFO 级别的信息,另外这里指定了 format 格式的字符串,包括 asctime、name、levelname、message ,lineno五个内容,分别代表运行时间、模块名称、日志级别、日志内容,语句所在的代码行,这样输出内容便是这四者组合而成的内容了,这就是 logging 的全局配置。
默认情况下Python的logging模块将日志打印到了标准输出中,且只显示了大于等于WARNING级别的日志,这说明默认的日志级别设置为WARNING(日志级别等级CRITICAL > ERROR > WARNING > INFO > DEBUG),默认的日志格式为日志级别:Logger名称:用户输出消息。(通过basicconfig设置输出格式)

import logging
 
logging.basicConfig(filename="file1.log",  #创建接收日志文件
                    level=logging.DEBUG,  #设置打印日志级别
                    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(lineno)s',  #输出日志格式
                    datefmt='%Y/%m/%d %H:%M:%S')  #输出时间格式
                    
# logger = logging.getLogger(__name__)
logger.info('This is a log info')
logger.debug('Debugging')
logger.warning('Warning exists')
logger.info('Finish')

在wapper.py中设置日志

import logging
from selenium.webdriver.common.by import By

def handle_black(func):
    logging.basicConfig(filename="file1.log",level=logging.INFO)  # 设置日志级别

    def wrapper(*args,**kwargs):
        from appium_xueqiu.page.base_page import BasePage
        black_list = [(By.XPATH,'//*[@text="行情"]')]  # 黑名单
        _max_num = 3
        _error_num = 0
        instance: BasePage = args[0] # 这里的args[0]是被调方法的第一是参数,比如这里我们要在find方法上添加这个方法,那这个instance就是find的self,指的对象就是BasePage
        try:
            logging.info('运行信息' + func.__name__ + "\n args参数: \n" + repr(args[1:]) + '\n' + repr(kwargs))
            element = func(*args,**kwargs)  # 这里执行装饰器要用方法
            return element
        except Exception as e:
            logging.error("这是error信息")
            for black in black_list:
                logging.info(black)
                ele_list = instance._driver.find_elements(*black)
                if _max_num > _error_num:
                    ele_list[0].click()
                    # 点击掉黑名单后,再次寻找
                    _error_num += 1
                    return wrapper(*args,**kwargs)
            raise e
    return wrapper
截图

basepage.py中封装screenshot方法

    def screenshot(self,name):
        self._driver.save_screenshot(name)

导入到Allure报告中

            self.screenshot("tmp.png")
            with open("tmp.png","rb") as f:
                content = f.read()
            allure.attach(content,attachment_type=allure_attachment_type.PNG)

在这里插入图片描述

录屏

第一种方式:直接使用AVD中的录屏方法,但是受到很多的限制。
第二种方式:开源组件scrcpy 地址:https://github.com/Genymobile/scrcpy
安装:brew install scrcpy -> brew install --cask android-platform-tools
使用:配置环境变量后直接在终端输入 scrcpy
录制视频到文档下

scrcpy --record ./result/tmp.mp4

使用到pytest中我们使用fixture,配置到conftest.py文件
conftest.py

import os
import signal
import subprocess
import shlex
import pytest

@pytest.fixture(scope="class",autouse=True)  # 注意不要使用scope还要设置function级别,因为CTRL_C会直接中断用例。·
def record():
    cmd = shlex.split("scrcpy --record 路径/文件.mp4")   # 执行 shell命令
    p = subprocess.Popen(cmd,shell=False,stdout=subprocess.PIPE,stderr=subprocess.STDOUT) # 启动一个子程序,并且不使用shell运行,输出正常或错误
    yield
    os.kill(p.pid,signal.CTRL_C_EVENT) # 退出进程

终端执行:appium --session-override 运行appium session会话不会超时,重复使用session

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值