当前位置: 首页 > news >正文

pytest+requests+Allure自动化框架

框架搭建思路:
使用python第三方requests库来处理接口的请求构建与响应结果获取
使用pytest来组建测试用例和执行测试用例脚本
使用yaml文件来编写请求数据和响应结果检查数据
使用pytest中的@pytest.mark.parameterize作为测试用例的加载引擎(参数化)
使用Allure作为结果的收集与展示

框架的大致执行流程:
读取yaml测试数据-->生成测试用例-->执行测试用例-->生成Allure报告
框架目录层级
image

1.yaml数据文件和配置文件
yaml数据文件格式如下:
image
配置文件config.ini定义了项目所有配置相关信息
image
config.py模块包含解析配置文件,添加和修改配置文件中的配置信息
"""封装获取conf文件类"""
CONFIG_BASE=os.path.dirname(os.path.abspath(file))
class Config:
def init(self,filename):
# 传入一个文件(ini格式)
self.filename=filename
# 创建自带的工具configparser(专门处理ini格式)来处理这个文件
self.cf=configparser.ConfigParser()
# 因为这个工具改变原来的大小写optionxform,所以要用这个工具自带的方法来不改变大小写
self.cf.optionxform=str
# 读取文件内容(要给文件路径,文件名)
self.cf.read(CONFIG_BASE,filename)
def get_runtime(self,section,option):
"""获取扇区内容-需要给扇区和选项,返回相应值"""
self.cf.get(section,option)
def set_runtime(self,section,option,value):
"""给指定的扇区设置对应的选项和值"""
self.cf.set(section,option,value)
# set方法的作用是:在内存中修改配置(还没写到文件里)
# 直接操作文件就像「用记事本手动改配置」,容易出错且麻烦;
# 而 set 方法配合 configparser 就像「用专门的配置编辑工具」,会自动处理格式、保留原有内容、应对各种特殊情况,让代码更简洁、可靠
# 所以先用set方法,然后再对配置文件进行添加和修改
with open(CONFIG_BASE,'w+') as f:
self.cf.write(f)
def add_section(self, section):
"""添加一个新扇区"""
## 向配置中添加一个新的section
self.cf.add_section(section)
#以读写的方式打开文件
with open(self.log_path, 'w+') as f:
#将配置写入文件返回值
return self.cf.write(f)
#self.cf.add_section(扇区):用于添加一个新的配置节 (扇区),比如在 INI 文件中添加 [Database] 这样的节
#如果要设置键值对,必须先有对应的节,否则会报错
#self.cf.set(section, option, value):用于在指定的节中设置键值对,比如在 [Database] 节中设置 host=localhost

def get_runtime(self, option):return self.get_conf("runtime", option)def get_server(self, option):return self.get_conf("server", option)def get_db_test(self, option):return self.get_conf("db_test", option)def get_email(self, option):return self.get_conf("email", option)

2.common文件夹包含框架所用的基础模块信息:
image

consts.py模块,包含项目中所有的常量信息
image

datas.py模块包含获取yaml数据文件的函数
具体代码如下:
"""读取yaml文件数据"""
import yaml
def get_yaml(file):
"""获取yaml文件内容"""
with open(file,encoding='utf8') as f:
yaml.safe_load(f)

def get_yaml_ids(file):
"""获取yaml文件里面的case_desc的值==ids"""
#调用get_yaml()方法获取yaml里面的内容作为数据
datas=get_yaml(file)
#循环遍历数据里面的值,遍历一次返回一次case_desc的值
for data in datas:
#之所以使用yield,因为yield不会一次性返回所有case_desc的值,调用一次返回一次
yield ['case_desc']
#yield的作用是将函数变成一个装饰器,每迭代一次,返回一个值
def ids_data(file):
"""将返回的ids值处理成字典形式"""
datas=get_yaml(file)
ids=get_yaml_ids(file)
return {"argvalues":datas,"ids":ids}
#使用ids_data(file)方法,需要在用例里面给出文件地址,然后使用参数化形式将ids值传入
#但是传入时需要解构(ids_data(file)),这样ids的值就是ids,argvalues的值是datas
#例如:file=os.path.join(文件地址,文件名),pytest.mark.parametrize(‘datas’,
ids_data(file))

log.py模块定义一个日志类,类中定义了日志内容的格式以及两种日志输出方式,一种是输出到文件,一种是输出到控制台
具体代码如下:
"""
配置log输出格式 time - loglevel - file - func - line - msg
支持输出到log文件及屏幕
支持返回一个logger
"""

import time
import logging
import os
from common.consts import BASE_PATH as base_path
from config.config import Config

class Log:
# 类方法。不需要实例化,可直接调用
@classmethod
# 不允许调用
def _config(cls):
# 读取配置文件方法Config()
c = Config()
#日志文件路径
log_dir = os.path.join(base_path, c.get_runtime("log_dir"))
#用当前时间年月日来命名
now = time.strftime("%Y-%m-%d", time.localtime(time.time()))
#拼接成完整的日志文件路径
log_file = os.path.join(log_dir, now + ".log")

    # 获取一个标准的logger, 配置loglevelcls.logger = logging.getLogger()cls.logger.setLevel(eval("logging." + c.get_runtime("log_level").upper()))# 建立不同handlerfile_handler = logging.FileHandler(log_file, mode="a", encoding='utf-8')console_handler = logging.StreamHandler()# 定义输出格式format = logging.Formatter("%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s")file_handler.setFormatter(format)console_handler.setFormatter(format)# 把定制handler 添加到我们loggercls.logger.addHandler(file_handler)cls.logger.addHandler(console_handler)@classmethod
#类方法,第一个参数通常命名为 cls,代表类本身(而不是实例)
#可以通过类名直接调用(如 ClassName.get_logger()),也可以通过实例调用
#提供一个统一的入口,获取已经配置好的日志记录器(logger)实例
def get_logger(cls):cls._config()#返回类的 logger 属性,即已经配置好的日志记录器实例。#这里的 cls.logger 通常是类级别的属性(在类中定义,所有实例共享),用于统一管理日志操作。return cls.logger

mail.py模块定义了一个发送邮件的方法,邮件发送内容是将项目生成的报告打包当作邮件的附件发送出去
具体代码如下:

读取配置文件
c=Config()

获取已经配置好的日志记录器(logger)

logger=Log.get_logger()

发送邮件
def send_email(report):
msg=MIMEMultipart()
msg['Subject']=c.get_email('subject')
msg['From']=c.get_email('user')
msg['To']=c.get_email('receiver')
配置邮件正文
content = c.get_server("report_url")
mime_text = MIMEText(content, 'plain', 'utf-8')
msg.attach(mime_text)

report_path=os.path.join(REPORT_PATH,report)
with open(report_path,"rb") as f:body=f.read()attach=MIMEBase('zip','zip',filename=report)attach.add_header('Content-Disposition','attachment',filename=('gb2312','',report))attach.set_payload(body)encoders.encode_base64(attach)msg.attach(attach)try:
# 初始化SMTP对象(QQ邮箱需用SMTP_SSL,端口465)smtp = smtplib.SMTP_SSL(c.get_email( "server"), 465)  # 直接连接服务器+端口# 登录邮箱(用户+授权码)smtp.login(c.get_email( "user"),  # 发件人邮箱c.get_email("password")  # QQ邮箱需用授权码,不是登录密码)# 发送邮件(发件人、收件人、邮件内容)smtp.sendmail(msg["From"],msg["To"].split(','),  # 收件人转列表(支持多个)msg.as_string())except Exception as e:print(e)print('发送失败')logger.error('邮件发送失败,请检查邮箱')
else:print('发送成功')logger.info('邮件发送成功')
finally:smtp.quit()

shell.py模块定义了一个执行windows命令的函数:
具体代码如下:
"""
封装shell命令
"""

import subprocess

class Shell:
@staticmethod
def execute(cmd):
output, errors = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
out_msg = output.decode("gbk")
return out_msg

tools.py模块包含一些项目用到的其他工具函数,包含md5加密、转换字符串、复制报告到服务器上

具体代码如下:
def md5_hash(password):
"""md5加密"""
return hashlib.md5(password.encode('utf-8')).hexdigest()

def read_yaml(filename):
"""获取yaml文件内容"""
with open(filename, encoding='utf-8') as f:
return yaml.safe_load(f)

def md5_code(passwd):
"""md5加密"""
md5 = hashlib.md5()
md5.update(passwd.encode("utf-8"))
return md5.hexdigest()

def trans_str(s):
"""转换字符串"""
return s.replace('{', '').replace('}', '').replace('": "', '=').replace('": ', '=').replace('":', '=').replace('"','').replace(',', ' ').strip()

def zip_dir(zip_file, start_dir=HTML_REPORT_PATH):
# 压缩文件夹
_zip_file = os.path.join(REPORT_PATH, zip_file)
if os.path.exists(_zip_file):
# 先删除
os.remove(_zip_file)
z = zipfile.ZipFile(_zip_file, 'w', zipfile.ZIP_DEFLATED)
for dir_path, dir_names, file_names in os.walk(start_dir):
fpath = dir_path.replace(start_dir, '') # 不replace的话,就从根目录开始复制
fpath = fpath and fpath + os.sep or ''
for filename in file_names:
z.write(os.path.join(dir_path, filename), fpath + filename)
z.close()

def copy_report():
"""复制一份报告到服务器上"""
source = HTML_REPORT_PATH
target = Config().get_server('www')
# 保证 target不存在
if os.path.exists(target) and os.path.isdir(target):
# 删除
shutil.rmtree(target)
# 开始拷贝
shutil.copytree(source, target, copy_function=shutil.copy2)

if name == 'main':
copy_report()

3.测试用例关联

很多时候,某一个接口要依赖接他接口,需要先执行其他接口(conftest.py模块里面的)后拿到返回值,再来调用新的接口,比如修改个人用户信息接口依赖登录后的token(使用fixture)
具体代码如下:
@pytest.fixture
def token():
"""拿到token,写成fixture,如果有用例需要使用token
直接在用例后面跟上拿取token的方法就行了,这就是脚手架,
不需要调用,只需要传入就可以使用"""
#准备数据
url = 'http://api.yesapi.net/api/App/User/Login'
method = 'post'
payload = {
"app_key": "704D5742A6416129CF6F25DFE61848DD",
"username": "ljj",
"password": "e10adc3949ba59abbe56e057f20f883e"
}
#发送请求
r=requests.get(url=url,params=payload)
#接收请求
#解析响应
#r.json()返回的是body里面的内容
body = r.json()
#r,status.code返回的是code状态码
code = r.status_code
#拿token
#token在返回值data里面,在body里面拿到data返回值,
#再从data返回值拿去token。使用 .get()方法拿取
body.get("data").get("token")

拿到token
接下来编写yaml测试用例信息
例如:
image

然后在cases文件夹编写以test_*.py命名的测试用例,用例中测试用例运行之前要先调用conftest.py模块中的token
fixture函数获取到token并完成接口请求

用例举例如下:
data_file = os.path.join(DATA_PATH, "test_2005_vip_login.yaml")
class TestVipLogin:
@pytest.mark.parametrize("datas", **datas_ids(data_file))
def test_login(self, datas, token):
url = datas.get('url')
method = datas.get('method')
payload = datas.get('payload')
check = datas.get('check')

    # 如果配置需要token但未提供,则使用传入的tokenpayload['token'] = token# 发送请求if method == 'GET':r = requests.get(url, params=payload)else:r = requests.post(url, data=payload)# 将响应转换为字符串response_text = r.text# 检查每个断言条件for condition in check:if '=' in condition:key, expected_value = condition.split('=', 1)# 构建JSON路径查找if key == 'err_msg':# 特殊处理错误消息,可能在msg字段或data.err_msg字段if f'"msg":"{expected_value}"' in response_text:continueif f'"err_msg":"{expected_value}"' in response_text:continuepytest.fail(f"未找到 {key}={expected_value},实际响应: {response_text}")else:# 检查普通字段if f'"{key}":{expected_value}' in response_text:continueif f'"{key}":"{expected_value}"' in response_text:continuepytest.fail(f"未找到 {key}={expected_value},实际响应: {response_text}")else:# 纯文本检查if condition not in response_text:pytest.fail(f"未找到文本 '{condition}',实际响应: {response_text}")

if name == 'main':
pytest.main(['-s', file])

4.执行模run.py

最后编写测试用例执行模块run.py,执行用例并生成报告,并将报告作为附件通过邮件发送出去

具体代码如下;
if name == 'main':
conf = config.Config()
log = log.Log.get_logger()
log.info('初始化配置文件, path=' + conf.log_path)

shell = shell.Shell()
xml_report_path = consts.XML_REPORT_PATH
html_report_path = consts.HTML_REPORT_PATH
cases_path = consts.CASE_PATH
attach_file = 'report.zip'# 定义测试集
args = ['-s', '-q', '--alluredir', xml_report_path, cases_path]
pytest.main(args)cmd = 'allure generate {} -o {} --clean'.format(xml_report_path, html_report_path)try:xx = shell.execute(cmd)
except Exception:log.error('执行用例失败,请检查环境配置')raise
# 压缩report文件夹
zip_dir(attach_file)
# 准备服务,拷贝报告文件夹到wamp服务中。
copy_report()
try:mail = mail.send_email(attach_file)
except Exception as e:log.error('发送邮件失败,请检查邮件配置')raise
http://www.sczhlp.com/news/65637/

相关文章:

  • C# 接口案例
  • 马鞍山做网站的公司78世界500强企业排名前十名
  • 网络营销平台的优势沧州快速关键词排名优化
  • 如何设计网站中的上传功能wordpress 在线生成app
  • 国外域名抢注网站淘宝客推广一天80单
  • 如何做网站跳转登入太原的网站建设公司哪家好
  • 专业的单位网站开发婚恋网站开发
  • 佛山市门户网站建设公司电商说白了做啥
  • 企业网站程序带wapseo外包多少钱
  • 长沙网页培训青岛网站推广优化公司
  • 网站右侧浮动窗口报关做业务可以上哪些网站
  • Firefox PDF查看器中表单填充与无障碍功能的实现技术解析
  • 无标注数据预测人脸识别模型偏差方法
  • qoj2625 Junk or Joy
  • 设计网官方网站北京百度推广
  • 营销型网站是什么样的专业网站制作 广州番禺
  • 51zwd一起做网站网站做多长时间才会成功
  • 上海网站制作公司怎么找me微擎怎么做网站
  • 网站设计计划书网站备案核验点
  • wordpress 多站点插件嵌入式开发工程师需要学什么
  • 域名买了怎么做网站wordpress标签生成图片
  • 企业网站手机版模板免费下载小程序商城开发平台
  • 前端招聘网站wordpress媒体库远程上传
  • 企业电器网站建设方案网站开发代码规范
  • 网站js修改头像代码网站首页图片素材长图大全
  • 替别人做网站app软件开发的费用和流程
  • 怎么把网站提交wordpress云储存
  • 温州做网站多少钱wordpress自动链接
  • ie里面没有重置的按钮
  • 智能ai写作免费网站网站是做响应式还是自适应的好