测试框架思想
为什么要使用框架
业务场景复杂
录制/回放无法适应复杂场景
自动化脚本工作量大且可维护性差
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) # 退出进程