前言
python:编程语言
pytest:独立的、全功能的python单元测试框架
selenium:用于web应用程序测试的工具
allure:测试报告展示
ddt:数据驱动
一、前置条件
1. 安装python开发环境
1.1 python解释器
3.10版本
1.2 pycharm集成开发环境
社区版
2.下载览器驱动
下载浏览器驱动,浏览器驱动版本要与浏览器版本一致。
下载地址:
Chrome:http://npm.taobao.org/mirrors/chromedriver/
Firefox:https://github.com/mozilla/geckodriver/releases
Edge:https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/
Safari:https://webkit.org/blog/6900/webdriver-support-in-safari-10/
- 浏览器驱动加到python环境变量中: path
- 或者chromedriver.exe与python.exe放在同一个目录
- 运行时也可以指定Chromedriver的路径本项目中将驱动chromedriver.exe放到工程的driver目录下-固定目录。
注:MCX调度台 查看浏览器驱动的版本
ctrl + F12 调出element窗口
点击Console 输入 'navigator.userAgent' 注意区分大小写
3.安装allure
1)下载allure,下载网址 https://github.com/allure-framework/allure2/releases
2)解压到电脑本地(此目录后续使用,不要动),配置环境变量(把allure目录下的bin目录配置到系统环境变量中的path路径)
参考《allure测试报告介绍》。
链接待添加。
4. 下载第三方库
工程新建requirements.txt,输入以下内容,之后安装requirements.txt文件,或单个安装
pytest== 7.4.2
selenium== 3.141.0
urllib3==1.26.18
PySocks=1.7.1=6.0.1
PyYAML
psutil=5.9.7=2.13.2
allure-pytest
注意:urllib3库与selenium版本之间的兼容性。
二、测试框架整体目录
三、测试工具类utils
3.1 管理时间timer.py
包括时间戳,日期等其他模块会使用的字符串,将时间封装成一个单独的模块,让其他模块来调用。
在 utils目录新建timer.py模块
"""
@File : timer.py
@Author : Sarah
@Date : 2023/7/10
@Desc :时间字符串管理
"""import time
import datetime
from functools import wraps
deftimestamp():"""时间戳"""return time.time()defdt_strftime(fmt="%Y%m"):"""
datetime格式化时间
:param fmt “%Y%m%d %H%M%S
"""return datetime.datetime.now().strftime(fmt)defsleep(seconds=1.0):"""睡眠时间"""
time.sleep(seconds)defrunning_time(func):"""函数运行时间"""@wraps(func)defwrapper(*args,**kwargs):
start = timestamp()
res = func(*args,**kwargs)print("校验元素done!用时%.3f秒!"%(timestamp()- start))return res
return wrapper
if __name__ =='__main__':print(dt_strftime("%Y%m%d %H%M%S"))
3.2 日志管理logger.py
测试脚本需要记录运行日志,所以将日志管理封装成一个模块,用于被其他模块调用。
在utils目录中新建logger.py模块,代码如下:
"""
@File : logger.py
@Author : Sarah
@Date : 2023/7/10
@Desc :日志管理模块
"""import logging
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
from config.config import cm
classLog:def__init__(self, seg):
self.seg = seg
# 创建logger
self.logger = logging.getLogger()ifnot self.logger.handlers:
self.logger.setLevel(logging.DEBUG)# 定义日志文件大小为10M
max_bytes =10*1024*1024# 定义保留的文件数量为50个
backup_count =50# 创建日志文件处理器,备份文件命名规则默认为添加后缀数字,数字越大时间越早if self.seg =="time":
fh = TimedRotatingFileHandler(cm.log_file, when='D', backupCount=backup_count, encoding='utf-8')"""
:param:when
'S':每秒切分日志文件
'M':每分钟切分日志文件
'H':每小时切分日志文件
'D':每天切分日志文件(默认值)
'W0' ~ 'W6':每星期切分日志文件(0表示星期一,1表示星期二,以此类推)
'midnight':每天午夜切分日志文件 与'D'相同
:param:interval 默认1,1D就是一天
"""
fh.setLevel(logging.INFO)else:
fh = RotatingFileHandler(cm.log_file, maxBytes=max_bytes, backupCount=backup_count, encoding='utf-8')
fh.setLevel(logging.INFO)# 创建一个handle输出到控制台
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)# 定义输出的格式
formatter = logging.Formatter(self.fmt)
fh.setFormatter(formatter)
ch.setFormatter(formatter)# 添加到handle
self.logger.addHandler(fh)
self.logger.addHandler(ch)@propertydeffmt(self):return'%(levelname)s\t%(asctime)s\t[%(filename)s:%(lineno)d]\t%(message)s'# 使用时间分割log文件,可以修改when参数,建议在性能测试脚本中使用# log = Log('time').logger# 使用大小分割log文件
log = Log('size').logger
四、配置文件夹config
4.1 公共配置config.py
将所有的文件目录,文件位置,封装后被其他模块调用,如果新增文件或新增目录,在这里添加方法。
在config目录下新建模块config.py,代码如下:
"""
@File : config.py
@Author : Sarah
@Date : 2023/7/10
@Desc :工程的文件目录,文件位置
"""import os
from utils.timer import dt_strftime
classConfigManager:"""
管理项目目录,文件
"""
DIR_CURR = os.path.abspath(__file__)
DIR_CONF = os.path.dirname(os.path.abspath(__file__))# 项目路径
DIR_BASE = os.path.dirname(DIR_CONF)
DIR_COMM = os.path.join(DIR_BASE,"common")
DIR_UTIL = os.path.join(DIR_BASE,"utils")# 页面元素和页面对象路径
DIR_PAGE = os.path.join(DIR_BASE,'page_object')# 页面元素
DIR_ELEMENT = os.path.join(DIR_BASE,"page_element")
DIR_DRIVER = os.path.join(DIR_BASE,"driver")@propertydefweb_ini_file(self):""" web 配置文件"""
ini_file = os.path.join(self.DIR_CONF,'web_cfg.ini')ifnot os.path.exists(ini_file):raise FileNotFoundError("配置文件%s不存在!"% ini_file)return ini_file
@propertydefdir_report_json(self):"""allure报告目录 json文件"""
report_dir = os.path.join(self.DIR_BASE,'report')
os.makedirs(report_dir, exist_ok=True)
json_dir = os.path.join(report_dir,'json')
os.makedirs(json_dir, exist_ok=True)return json_dir
@propertydefdir_report_html(self):"""allure报告目录 html文件"""
report_dir = os.path.join(self.DIR_BASE,'report')
os.makedirs(report_dir, exist_ok=True)
html_dir = os.path.join(report_dir,'html')
os.makedirs(html_dir, exist_ok=True)return html_dir
@propertydefdir_log(self):"""日志目录"""
log_dir = os.path.join(self.DIR_BASE,'logs')
os.makedirs(log_dir, exist_ok=True)return log_dir
@propertydeflog_file(self):"""日志文件"""return os.path.join(self.dir_log,'{}.log'.format(dt_strftime()))@propertydefdir_img(self):"""截图目录"""
img_dir = os.path.join(self.dir_log,'images')
os.makedirs(img_dir, exist_ok=True)return img_dir
@propertydefimg_file(self):"""截图文件"""return os.path.join(self.dir_img,'{}.png'.format(dt_strftime('%Y%m%d%H%M%S')))@propertydefdir_testdata(self):"""测试数据目录"""
test_data_dir = os.path.join(self.DIR_BASE,'testdata')
os.makedirs(test_data_dir, exist_ok=True)return test_data_dir
@propertydefdir_web_testdata(self):"""测试数据目录-web"""
test_data_dir = os.path.join(self.dir_testdata,'web')
os.makedirs(test_data_dir, exist_ok=True)return test_data_dir
@propertydeftest_web_yaml_file(self):""" 测试数据文件.yaml """
yaml_file = os.path.join(self.dir_web_testdata,'test_data.yaml')ifnot os.path.exists(yaml_file):raise FileNotFoundError("测试数据文件%s不存在!"% yaml_file)return yaml_file
@propertydeftest_web_json_file(self):""" 测试数据文件.json """
json_file = os.path.join(self.dir_web_testdata,'test_data.json')ifnot os.path.exists(json_file):raise FileNotFoundError("测试数据文件%s不存在!"% json_file)return json_file
@propertydefweb_driver(self):"""浏览器驱动"""
os.makedirs(self.DIR_DRIVER, exist_ok=True)
driver = os.path.join(self.DIR_DRIVER,'chromedriver.exe')ifnot os.path.exists(driver):raise FileNotFoundError("浏览器驱动%s不存在!"% driver)return driver
cm = ConfigManager()
4.2 web配置文件web_cfg.ini
在config目录下新建web_cfg.ini(文件名与config.py中封装的要一致), 用于存放web测试的参数,举例如下:
# web_cfg.ini[host]
HOST = http://IP/index.action
[driver]# 驱动进程名称
DRIVER_PROCESS = chromedriver.exe
五、测试公共库common
5.1 公共方法common.py
pytest框架的前后置,运行时自动调用。
在common目录下新建模块common.py,这里的前后置仅为log打印,查阅log比较方便,代码如下:
"""
@File :common.py
@Author : Sarah
@Date : 2023/7/10
@Desc :用例执行前后置,pytest框架
"""from utils.logger import log
classCommon:@staticmethoddefsetup_class():
log.info('-------module start')@staticmethoddefteardown_class():
log.info('-------module end')@staticmethoddefteardown_method():
log.info('-------case end')
5.2. 读取配置文件read_cfg.py
配置文件使用ini格式,封装读取ini的方法。
在common目录下新建文件read_cfg.py,代码如下:
"""
@File :read_cfg.py
@Author : Sarah
@Date : 2023/7/10
@Desc :封装读取ini配置文件
"""import configparser
from config.config import cm
# 根据实际定义
HOST ='host'
DRIVER ='driver'classReadConfig:"""ini配置文件"""def__init__(self, ini_file):
self.file= ini_file
self.config = configparser.RawConfigParser()# 当有%的符号时使用Raw读取 不对配置文件中的值进行解析
self.config.read(self.file, encoding='utf-8')def_get(self, section, option):"""获取"""return self.config.get(section, option)def_set(self, section, option, value):"""更新"""
self.config.set(section, option, value)withopen(cm.ini_file,'w')as f:
self.config.write(f)@propertydefweb_url(self):return self._get(HOST,'HOST')@propertydefweb_driver_process(self):return self._get(DRIVER,'DRIVER_PROCESS')
web_cfg = ReadConfig(cm.web_ini_file)
涉及配置和数据驱动的后续添加。
六、PO模式封装
按照POM模式设计,将自动化设计的页面或模块封装成对象,以便代码复用,便于维护(如果元素信息发生变化了,不用修改测试用例脚本)。
6.1.结构
- base(基类):page页面的一些公共方法
- page(页面):一个页面封装成一个对象
- element(元素):页面元素,本项目使用.py来存放页面元素,写代码方便
- testcase(测试用例):存放测试用例
6.2 基类page_base
在page_base目录下新建page_base.py文件,代码如下:
"""
@File :page_base.py
@Author :sarah
@Date :2023-07-21
@Desc :Base类:存放所有Page页面公共操作方法
"""from selenium.webdriver.support.select import Select
from selenium.webdriver.support.wait import WebDriverWait
from utils.logger import log
from utils.timer import sleep
from selenium.webdriver.common.action_chains import ActionChains
classBase:def__init__(self, driver):
self.driver = driver
defbase_find(self, loc, timeout=30, poll=0.5):"""
查找元素
timeout:超时的时长,一般30S,超时未找到报错
poll:检测间隔时长,默认0.5s,如果有一闪而逝的提示信息框,修改为0.1s
"""return WebDriverWait(self.driver, timeout, poll).until(lambda x: x.find_element(*loc))defbase_input(self, loc, text):"""输入文本"""
el = self.base_find(loc)
el.clear()if text isnotNone:
el.send_keys(text)
log.info(f"{el}输入文本:{text}")defbase_click(self, loc):"""点击"""
self.base_find(loc).click()
sleep()
log.info(f'点击按钮:{loc}')defbase_get_text(self, loc):"""获取当前元素文本"""
_text = self.base_find(loc).text
log.info(f"获取文本:{_text}")return _text
defbase_get_title(self):"""获取当前页title"""
title = self.driver.title
log.info(f"当前页面:{title}")return title
defbase_alert_confirm(self):"""自动确认弹框 以便继续进行后续的测试操作"""
self.driver.switchTo().alert().accept()defbase_is_dis(self, loc):"""查看元素是否可见"""
state = self.base_find(loc).is_displayed()
log.info(f"获取元素可见状态:{state}")return state
defbase_keep_press(self, loc, time):"""保持长按"""
ActionChains(self.driver).click_and_hold(self.base_find(loc)).perform()
log.info(f"长按:{loc}")
sleep(time)
ActionChains(self.driver).release(self.base_find(loc)).perform()
log.info(f"释放:{loc}")defbase_select(self, loc, text):"""
下拉框选择
:param loc: select标签元素,父类, 不是option
:param text: 通过显示文本选择
"""
Select(self.base_find(loc)).select_by_visible_text(text)
log.info(f"选择下拉框{loc}的{text}")defbase_tick_checkbox(self, loc, num):"""勾选框"""
checkbox_list = self.base_find(loc)
action = ActionChains(self.driver)for i inrange(0, num):
action.move_to_element(checkbox_list[i])
action.click(checkbox_list[i]).perform()
sleep()defbase_invisible_element(self, loc, num, stop):"""对动态变化元素执行动作链"""
msg = self.base_find(loc)for i inrange(0, num):
action = ActionChains(self.driver)
action.move_to_element(msg[i])
action.click(msg[i])
action.perform()
sleep(stop)defbase_refresh(self):"""刷新页面F5"""
self.driver.refresh()defbase_quit(self):"""退出浏览器"""
self.driver.quit()
6.3. 页面元素page_element
在page_element目录下新建el_xxx.py文件,每个py文件存储一个页面的元素信息。
比如新建el_login.py,存放login页面的元素信息,代码如下:
"""
@File :el_login.py
@Author :sarah
@Date :2023-07-21
@Desc :登录界面的元素信息
"""from selenium.webdriver.common.by import By
byid = By.ID
byname = By.NAME
bycname = By.CLASS_NAME
bytname = By.TAG_NAME
bylink = By.LINK_TEXT
bycss = By.CSS_SELECTOR
byxpath = By.XPATH
# 登录界面元素配置信息
login_window = byxpath,'//*[@id="login"]/div/div[1]/div'
login_username = byxpath,'//*[@placeholder="用户名"]'
login_pwd = byid,'password'
login_err_info = bycss,'.el-message__content'
login_btn = byxpath,'//*[text()="登录"]'
logout_btn = byxpath,'//*[@id="login"]/div/div[2]/div/form/div[4]/div/span[3]'
logout_cancel = byxpath,'/html/body/div[2]/div/div[3]/button[1]'
logout_confirm = byxpath,'/html/body/div[2]/div/div[3]/button[2]'
6.4 页面对象
在page_object目录下新建page_xxx.py文件,每个py文件存储一个页面的信息。
比如新建page_login.py,存放login页面的封装信息,代码如下:
"""
@File :page_login.py
@Author :sarah
@Date :2023-07-21
@Desc :登录界面封装
"""import allure
from utils.timer import sleep
from page_base.page_base import Base
from page_element import el_login
classPageLogin(Base):"""登录页面"""defpage_input_username(self, username):"""输入登录用户名"""
self.base_input(el_login.login_username, username)
sleep()defpage_input_passwd(self, password):"""输入密码"""
self.base_input(el_login.login_pwd, password)
sleep()defpage_input_verify_code(self, verify_code):"""输入验证码"""passdefpage_click_login_btn(self):"""点击登录按钮"""
self.base_click(el_login.login_btn)
sleep()defpage_get_error_info(self):"""获取异常提示信息"""return self.base_get_text(el_login.login_err_info)defpage_click_err_btn_ok(self):passdefpage_click_quit_btn(self):"""点击退出按钮"""
self.base_click(el_login.login_btn)
sleep()defpage_click_quit_cancel(self):"""取消退出"""
self.base_click(el_login.logout_cancel)
sleep()defpage_click_quit_confirm(self):"""确定退出"""
self.base_click(el_login.logout_confirm)
sleep()defpage_login(self, username, password):
sleep(2)with allure.step(f"输入用户名: {username}"):
self.page_input_username(username)with allure.step(f"输入密码: {password}"):
self.page_input_passwd(password)
sleep(5)with allure.step("点击登录按钮"):
self.page_click_login_btn()
6.5 测试用例testcases目录结构
testcases目录下新建python包用户存储不同模块的测试用例,比如建立web文件夹和app文件夹,分别用户存储web测试用例和app测试用例。
web目录下新建test_login,test_group_call ……python包。
如果不需要也可以不划分testcases下的目录结构,划分目录结构除了查阅清晰之外,还为了方便conftest.py文件的自动运行。
6.6. conftest.py
在目录testcases下新建conftest.py文件,当中编写所有测试用例的前后置。
conftest.py的说明参考《pytest框架介绍》。链接待添加。
conftest.py中包含了web驱动,allure报告获取失败用例截图,打印用例名称,代码如下:
"""
@File :testcases\conftest.py
@Author :sarah
@Date :2023-07-21
@Desc :web测试用例的前置
"""import pytest
import psutil
import allure
from selenium import webdriver
from utils.timer import dt_strftime
from utils.logger import log
from common.read_cfg import web_cfg
from config.config import cm
web_driver [email protected]("浏览器驱动")@pytest.fixture(scope='session', autouse=True)defdrivers(request):global web_driver
if web_driver isNone:
option = webdriver.ChromeOptions()
option.binary_location =r'D:\HBFEC\AcroDCYJ\现场应急调度平台.exe'# 根据实际目录修改
web_driver = webdriver.Chrome(executable_path=cm.web_driver, options=option)@allure.step("退出浏览器驱动")deffn():
web_driver.quit()@pytest.fixture(scope='function', autouse=True)defclose_quit(self):
process_name = web_cfg.web_driver_process
# 查找所有的Chrome驱动进程
process_list =[process for process in psutil.process_iter()if process.name()== process_name]iflen(process_list)>0:# 如果有多个Chrome驱动程序正在运行,则杀死所有的Chrome驱动程序for process in process_list:
process.kill()
log.info('存在Chrome驱动程序,并且已杀死所有Chrome驱动程序')else:
log.info('没有Chrome驱动程序正在运行')@pytest.fixture(scope='function', autouse=True)defreconnect(self):
process_name = web_cfg.web_driver_process
# 查找所有的Chrome驱动进程
process_list =[process for process in psutil.process_iter()if process.name()== process_name]iflen(process_list)>0:# 如果有多个Chrome驱动程序正在运行,则杀死所有的Chrome驱动程序if process_list[0]!= process_list[1]:
fn()else:
log.info('没有Chrome驱动发生重启')
request.addfinalizer(fn)with allure.step("返回浏览器驱动"):return web_driver
@pytest.hookimpl(tryfirst=True, hookwrapper=True)defpytest_runtest_makereport(item, call):# 后置,获取测试结果
outcome =yield
reps = outcome.get_result()if reps.when =='call'and reps.failed:# 在测试失败时进行截图, 添加到allure报告中
img = web_driver.get_screenshot_as_png()# 为截图文件命名
name ='_'.join([reps.nodeid.replace('testcases/','').replace('::','_'), dt_strftime('%Y%m%d %H%M%S')])
allure.attach(img, name=name, attachment_type=allure.attachment_type.PNG)@pytest.hookimpl(tryfirst=True, hookwrapper=True)defpytest_runtest_call(item):# 记录正在运行的用例
called = item.nodeid.replace('testcases/','').replace('::','_')
log.info('case:%s', called)yield
6.7 测试用例test_xxx.py
在testcases或testcases的子目录下新建test_xxx.py文件,编写测试用例,调用封装的页面的方法。
举例:
在testcases下新建test_login.py代码如下:
"""
@File :test_login.py
@Author :sarah
@Date :2023-07-21
@Desc :登录模块测试用例
"""import allure
import pytest
from common.read_yaml import login_para
from utils.logger import log
from common.common import Common
from page_object.login import PageLogin
@allure.feature("登录功能")classTestLogin(Common):"""登录测试 """@allure.story("登录失败")@allure.title("登录检测-{login_case}")@allure.description("测试登录时的各种异常情况")@allure.severity("minor")@pytest.mark.parametrize("login_case", login_para)deftest_login(self, drivers, login_case):
username = login_case['username']
password = login_case['password']
expect_text = login_case['expect']
log.info(f"username:{username}")
log.info(f"password: {password}")
log.info(f"expect text: {expect_text}")
PageLogin(drivers).page_login(username, password)with allure.step("捕获提示信息"):
error_info = PageLogin(drivers).page_get_error_info()with allure.step("开始断言"):assert expect_text in error_info
@allure.story("登录成功")@allure.title("登录成功")@allure.description("输入正确合法的用户名和密码登录成功")@allure.severity("critical")deftest_login_suc(self, drivers):"""需要增加测试步骤"""
log.info(drivers.title)
log.info("输入正确的用户名密码登录成功")
当中涉及数据驱动parametrize和allure报告,参考第七第八章节。
七(1)、数据驱动json
测试数据格式没有统一的要求,可以使用json格式。
json格式返回值可以是单参数或多参数,本章节介绍多参数的参数化。
7.1. 测试数据test_data.json
在testdata目录下新建测试数据文件test_data.json,举例如下:
# test_data.json{"login":[{"desc":"登录失败(用户不存在)","username":"700001","password":"700001@mcx","expect":"登录注册失败"},{"desc":"登录失败(密码为空)","username":"700002","password":"","expect":"失败"},{"desc":"登录失败","username":"700003","password":"700003","expect":"失败"}],"group_call":[{"desc":"组呼成功","group_id":"80001","time":"10","expect":"呼叫成功"},{"desc":"组呼成功","group_id":"80002","time":"10","expect":"呼叫成功"},{"desc":"组呼成功","group_id":"80003","time":"10","expect":"呼叫成功"}]}
7.2 读取json文件read_json.py(元祖列表)
在common目录下新建文件read_json.py,读取测试数据,供测试用例使用,代码如下:
"""
@File :read_json.py
@Author :sarah
@Date :2023-07-21
@Desc :读取json格式的测试数据
"""import json
from config.config import cm
classReadJson:def__init__(self, json_file):
self.json_file = json_file
defread_json(self, key):""" 返回元祖列表 """
arr =[]withopen(self.json_file,"r", encoding="utf-8")as f:
datas = json.load(f,)
value = datas.get(key)for data in value:
arr.append(tuple(data.values())[1:])return arr
json_para = ReadJson(cm.test_json_file)# 读取json文件中的对应模块,每个模块一个实例
login_para = json_para.read_json('login')
group_call_para = json_para.read_json('group_call')# 得到元祖列表格式如下:# [('700001', '700001', '登录成功'), ('700002', '', '登录失败,密码不能为空'), ('700003', '700003', '登录成功')]
7.3. 测试用例test_xxx.py(多参数的参数化)
上述读取json数据封装方法返回值为元祖列表,使用多参数驱动的方式。
举例:
在testcases目录下新建test_login.py文件,代码如下:
"""
@File :test_login.py
@Author :sarah
@Date :2023-07-21
@Desc :登录模块测试用例
"""import allure
import pytest
from common.read_json import login_para
from utils.logger import log
from common.common import Common
from utils.timer import sleep
from page_object.page_login import PageLogin
@allure.feature("登录功能")classTestLogin(Common):"""登录测试 """@allure.story("登录失败")@allure.title("登录异常检测-{username}-{password}-{expect_text}")@allure.description("测试登录时的各种异常情况")@allure.severity("minor")@pytest.mark.parametrize("username, password, expect_text", login_para)# 多参数的参数化,这样写的话参数可以直接使用,但在parametrize与测试函数的形参中需要列出所有的参数,并且参数的顺序必须一致deftest_login(self, drivers, username, password, expect_text):
PageLogin(drivers).page_login(username, password)with allure.step("捕获提示信息"):
error_info = PageLogin(drivers).page_get_error_info()with allure.step("开始断言"):assert expect_text in error_inf
七(2)、数据驱动yaml
测试数据格式没有统一要求,可以使用yaml格式。
本章节介绍单参数的参数化和多参数的参数化。
7.1 测试数据test_data.yaml
在testdata目录下新建测试数据文件test_data.yaml,举例如下:
login:- case1: 登录成功1
username:70001
password:7001@mcx
expect: 失败
- case2: 登录成功2
username:70002
password:
expect: 密码
- case3: 登录成功3
username:70003
password:70003
expect: 密码
login2:- case1: 登录成功1
expect: 失败
password:7001@mcx
username:70001- case2: 登录成功2
expect: 密码
password: null
username:70002
7.2. 读取yaml文件read_yaml.py 单参数-字典列表
在common目录下新建文件read_yaml.py,读取测试数据,供测试用例使用,代码如下:
"""
@File :read_yaml.py
@Author :sarah
@Date :2023-07-21
@Desc :读取yaml格式的测试数据,返回的是字典列表
"""import yaml
from config.config import cm
classReadYaml:def__init__(self, yaml_file):
self.yaml_file = yaml_file
defread_yaml(self, key):withopen(self.yaml_file,'r', encoding='utf-8')as f:
datas = yaml.load(f, Loader=yaml.FullLoader)# 避免报警告,需要加入Loader=yaml.FullLoader
value = datas[key]return value
yaml_para = ReadYaml(cm.test_yaml_file)
login_para = yaml_para.read_yaml('login')"""
返回字典列表
[{'case1': '登录成功1', 'username': 70001, 'password': '7001@mcx', 'expect': '失败'},
{'case2': '登录成功2', 'username': 70002, 'password': None, 'expect': '密码'},
{'case3': '登录成功3', 'username': 70003, 'password': 70003, 'expect': '密码', 'desc': '登录失败(用户不存在)'}]
"""
7.3 测试用例test_xxx.py-单参数参数化
上述读取yaml数据封装方法返回值为字典列表,使用单参数驱动的方式。
举例:
在testcases目录下新建test_login.py文件,代码如下:
"""
@File :test_login.py
@Author :sarah
@Date :2023-07-21
@Desc :登录模块测试用例
"""import allure
import pytest
from common.read_yaml import login_para
from utils.logger import log
from common.common import Common
from utils.timer import sleep
from page_object.page_login import PageLogin
@allure.feature("登录功能")classTestLogin(Common):"""登录测试 """@allure.story("登录失败")@allure.title("登录检测-{login_case}")@allure.description("测试登录时的各种异常情况")@allure.severity("minor")@pytest.mark.parametrize("login_case", login_para)deftest_login(self, drivers, login_case):
username = login_case['username']
password = login_case['password']
expect_text = login_case['expect']
log.info(f"username:{username}")
log.info(f"password: {password}")
log.info(f"expect text: {expect_text}")
PageLogin(drivers).page_login(username, password)with allure.step("捕获提示信息"):
error_info = PageLogin(drivers).page_get_error_info()with allure.step("开始断言"):assert expect_text in error_info
7.4 读取yaml文件read_yaml.py 多参数-元祖列表
在common目录下新建read_yaml.py,返回值为元祖列表,与read_json返回值相同,代码如下:
"""
@File :read_yaml.py
@Author :sarah
@Date :2023-07-21
@Desc :读取yaml格式的测试数据
"""import yaml
from config.config import cm
classReadYaml:def__init__(self, yaml_file):
self.yaml_file = yaml_file
defread_yaml(self, key):
arr =[]withopen(self.yaml_file,'r', encoding='utf-8')as f:
datas = yaml.load(f, Loader=yaml.FullLoader)# 避免报警告,需要加入Loader=yaml.FullLoader
value = datas[key]for data in value:
arr.append(tuple(data.values())[1:])return arr
yaml_para = ReadYaml(cm.test_yaml_file)
login_para = yaml_para.read_yaml('login')
7.5 测试用例test_xxx.py-多参数参数化
与7.3章节一致,只需要修改
from common.read_json import login_para
为
from common.read_yaml import login_para
7.6 对比单参数与多参数化
1. 装饰器@pytest.mark.parametrize
单参数,使用一个参数名,传入变量
@pytest.mark.parametrize("login_case", login_para)
多参数,使用多个参数名,并且需要保证参数的顺序与变量log_para中值的顺序一致。
@pytest.mark.parametrize("username, password, expect_text", login_para)
2. 函数def test_login参数
单参数,使用参数化parametrize中对应的一个参数名
deftest_login(self, drivers, login_case):
多参数,使用参数化parametrize中对应的多个参数名
deftest_login(self, drivers, username, password, expect_text):
3. allure装饰器@allure.title
想要在allure中展示参数化的信息,需要将参数化信息放到title中。
单参数 ,使用参数化parametrize中的一个参数名
@allure.title("登录检测-{login_case}")
多参数,使用参数化parametrize中对应的多个参数名
@allure.title("登录异常检测-{username}-{password}-{expect_text}")
4. 参数的使用
单参数,需要在函数中读取每个参数的value,并且由于log不展示参数,需要打印参数到log。
username = login_case['username']
password = login_case['password']
expect_text = login_case['expect']
log.info(f"username:{username}")
log.info(f"password: {password}")
log.info(f"expect text: {expect_text}")
多参数,直接使用形参,不需要读取或打印。
5. log打印
两种方式在打印测试用例名称上有所区别。
单参数,截取相关打印信息如下:
case:test_login.py_TestLogin_test_login[login_case0]
username:70001
password: 7001@mcx
expect text: 失败
case:test_login.py_TestLogin_test_login[login_case1]
username:70002
password: None
expect text: 密码
log中的测试用例名称为:函数名+参数名+编号,编号从0开始。
所以建议使用这种方式的话用log把对应的参数打印出来,方便调试。
多参数,截取相关打印信息如下:
case:test_login.py_TestLogin_test_login[700001-700001@mcx-登录注册失败]
case:test_login.py_TestLogin_test_login[700002–失败]
case:test_login.py_TestLogin_test_login[700003-700003-失败]
log中用例名称显示为函数名+参数值,可以不用将参数打印到log。
6. allure展示
单参数展示,测试用例标题不规则,参数不易读,如下图:
失败用例截图的名称不显示参数详情,如下图:
多参数展示,测试用例标题相对规则,参数详情清晰易读,如下图所示:
多参数失败用例截图展示测试参数,如下图所示:
根据上述对比选择合适的测试数据格式和参数化方式实现数据驱动。
八、allure报告
8.1. 报告内容 allure_des
在allure_des目录下新建allure_des.py文件,用于创建报告的环境信息,生成器信息和统计信息,代码如下:
"""
@File :allure_des.py
@Author :sarah
@Date :2023-07-21
@Desc :allure报告的描述信息,每次运行重新创建
"""import json
import os
import platform
import pytest
from config.config import cm
defset_report_env_on_results():"""
在json报告的目录下生成一个写入环境信息的文件:environment.properties(注意:不能放置中文,否则会出现乱码)
"""# 需要写入的环境信息,根据实际工程填写
allure_env ={'OperatingEnvironment':'mcx DC','PythonVersion': platform.python_version(),'PytestVersion': pytest.__version__,'Platform': platform.platform(),'selenium':'3.141.0','Browser':'Chrome','BrowserVersion':"59.0.3071.115",'DiverVersion':"2.32 MCX-driver"}
allure_env_file = os.path.join(cm.dir_report_json,'environment.properties')withopen(allure_env_file,'w', encoding='utf-8')as f:for _k, _v in allure_env.items():
f.write(f'{_k}={_v}\n')defset_report_executer_on_results():"""
在json报告的目录下生成一个写入执行人的文件:executor.json
"""# 需要写入的运行信息
allure_executor ={"name":"张三","type":"jenkins","url":"http://helloqa.com",# allure报告的地址"buildOrder":3,"buildName":"allure-report_deploy#1","buildUrl":"http://helloqa.com#1","reportUrl":"http://helloqa.com#1/AllureReport","reportName":"张三 Allure Report"}
allure_env_file = os.path.join(cm.dir_report_json,'executor.json')withopen(allure_env_file,'w', encoding='utf-8')as f:
f.write(str(json.dumps(allure_executor, ensure_ascii=False, indent=4)))defset_report_categories_on_results():"""
在json报告的目录下生成一个写入统计类型的文件:categories.json
"""# 需要写入的运行信息
allure_categories =[{"name":"Ignored tests","matchedStatuses":["skipped"]},{"name":"Infrastructure problems","matchedStatuses":["broken","failed"],"messageRegex":".*bye-bye.*"},{"name":"Outdated tests","matchedStatuses":["broken"],"traceRegex":".*FileNotFoundException.*"},{"name":"Product defects","matchedStatuses":["failed"]},{"name":"Test defects","matchedStatuses":["broken"]}]
allure_cat_file = os.path.join(cm.dir_report_json,'categories.json')withopen(allure_cat_file,'w', encoding='utf-8')as f:
f.write(str(json.dumps(allure_categories, ensure_ascii=False, indent=4)))
8.2. 测试报告report
在report目录下分别建立json目录和html目录,allure报告首先生成json文件,包括allure_des生成的json文件,之后再生成html报告,命令在runtest.py的run()函数中。
生成的报告显示如下:
九、pytest运行
9.1. pytest.ini
在工程目录下新建pytest.ini文件,用于保存用例执行参数,内容如下:
# pytest.ini[pytest]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support =True
addopts =-vs --clean-alluredir
testpaths =./testcases
python_files = test*.py
python_classes = Test*
python_functions = test_*
9.2. runtest.py
在工程目录下新建runtest.py文件,作为测试用例执行的main,代码如下:
"""
@File :runtest.py
@Author :sarah
@Date :2023-07-21
@Desc :测试用例执行的入口
"""import os
import pytest
from allure_des.allure_des import set_report_env_on_results, \
set_report_executer_on_results, \
set_report_categories_on_results
from config.config import cm
from utils.timer import sleep
defrun():
pytest.main(['--allure-stories=登录成功,登录失败','--alluredir=%s'% cm.dir_report_json])# 在json目录下创建categories.json文件
set_report_categories_on_results()# 在json目录下创建environment.properties文件
set_report_env_on_results()# 在json目录下创建executor.json文件
set_report_executer_on_results()
sleep(3)
os.system("allure generate %s -o %s --clean"%(cm.dir_report_json, cm.dir_report_html))if __name__ =='__main__':
run()
……结束。
版权归原作者 Sarah_0909 所有, 如有侵权,请联系我们删除。