跳到主要内容 基于Python+Requests+Allure搭建接口自动化框架 | 极客日志
目录
Python 中的args 与*kwargs 调用时,参数只需传递一个一个的值,自动会打包为元组 args 调用时,参数需要传递多个 键=值,自动打包为字典 kwargs 使用 requests 发送请求 发送 get 请求 发送 get 请求 请求方式一:requests.request(method, url, **kwargs) 请求方式二:requests.get(url, params=None, **kwargs) 发送 post 请求 data 与 json 参数区别: json 要求传字典,json.loads 将字符串转为字典,content-type 为 application/json data 要求字符串,json.dumps 将字典转为字符串,content-type 不是 json 一般为 application/x-www-form-urlencoded data 可以传字典等格式数据,只是会自动编码为字符串,如果传的本身是字符串,则不进行编码 上传文件 使用 session 发送请求 登录 登录后操作 jsonpath 使用 框架基本封装过程 目录介绍 yaml 用例 基本格式 读取 yaml 封装请求关键字 基本核心执行器 main 执行入口 -vs 详细内容 --capture=sys 系统配置 生成 stdout 附件 --clean-alluredir 清空报告报告数据,保证每次生成报告都使用最新的数据 --alluredir=allure-results 结果数据目录,生成的 原始测试结果文件(JSON 格式) ./core/test_runner.py 用例文件夹 生成测试报告 allure-report 最终测试报告目录,根据 allure-results 中数据创建报告 -o 指定目录 -c 先清空在生成 生成的报告只能在 pycharm 打开,需要安装 allure-combine npm install -g allure-combine 核心执行器优化 优化报告 调用该函数时,自动创建 step total=len(test_steps):告诉 tqdm 总共有多少步(用于计算百分比) desc='开始执行':初始描述文字(显示在进度条左边) with ... as pbar:使用上下文管理器,确保进度条正确关闭 pbar:进度条对象,后续可通过它动态更新内容 增加全局配置 yaml 用例文件改造 全局变量类 def readcontext(filepath): with open(file_path, 'r', encoding='utf-8') as file: data = yaml.load(file, Loader=yaml.SafeLoader) return data jinja2 的模版必须是字符串,且渲染后返回字符串 渲染 yml 文件,eval 将字符串转为原本字典 增加前置后置脚本执行 前置脚本执行 后置脚本执行 多用例执行 读取文件夹下符合规则的 yaml 文件 用例之间进行数据关联 断言操作 单个字段进行断言 整个响应结果进行断言 数据库断言 数据库链接 生成游标 游标执行 sql 游标获取结果 关闭游标,关闭数据库 yml 数据驱动 在模版渲染前,将 ddt 保存到全局变量中 allure 与 loguru 日志 基本使用 保存到文件中 日志级别:debug<info<warning<error 配置日志 创建日志管理器关联步骤日志 日志管理器 关联测试步骤与报告 session 复用机制 用于判断是否 session 复用,决定使用何种方式进行实例化关键字类 my_request = MyRequest(requests) 文件上传与下载 文件上传 文件下载 pytest 自定义插件 plugins,插件列表,通常传入类实例或模块 pytest 运行时,会自动扫描所有加载的插件(包括 conftest.py 和通过 plugins 参数传入的插件) 将钩子函数定义在自定义插件中 打包 main 文件内容替换 python 获取命令执行参数 方式一:sys.argv 是一个列表,第一个元素为脚本名称,后续元素为字符串参数 sys.argv 不会对参数进行解析,所有参数都是字符串形式 python main.py a=1 b=2 args=['main.py','a=1', 'b=2'] 方式二:使用 argparse 模块 argparse 可以定义与解析参数 1、创建解析器 2、添加参数,存在-(短格式),--(长格式)是可选参数,否则是必填参数 长短格式参数可以同时定义 长短格式同时定义,执行时任选一种就行 3、解析命令行参数,要解析必须定义上述添参数规则 python main1.py -a 1 hhh -c sss main 文件内容 配置日志 导出框架依赖包 增加 setup 文件 with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read() 示例执行命令 tc-api-test --type=yml --case=./testcases/yamlcases_01
Python
基于Python+Requests+Allure搭建接口自动化框架 基于Python结合Requests库发送HTTP请求,利用Pytest作为测试执行器,Allure生成报告,以及YAML进行数据驱动的接口自动化测试框架搭建过程。涵盖关键字封装、全局配置管理、用例数据关联、断言逻辑(含数据库)、日志集成、Session复用及文件上传下载等功能,并提供了项目打包部署方案。
Python 中的*args 与**kwargs
接收可变参数
*args:会将传入的额外位置参数打包成一个元组(tuple)
**kwargs:会将传入的额外关键字参数打包成一个字典(dict)
def my_tool (*args ):
print (args)
def ( ):
(kwargs)
__name__ == :
my_tool( , , )
my_tools(name= , age= )
my_tools
**kwargs
print
if
'__main__'
'hello'
2
True
'zs'
10
使用 ** 对字典进行解包,将键值对作为参数传递给函数
使用*对列表进行解包,将列表元素作为参数传递给函数
data = {'username' : 'zs' , 'password' : '123' }
names = ['zs' , 'ls' , 'wu' ]
def func1 (username, password ):
print (username + password)
def func2 (name1, name2, name3 ):
print (name1 + name2 + name3)
if __name__ == '__main__' :
func1(**data)
func2(*names)
使用 requests 发送请求 需要先安装:pip install requests
发送 get 请求
请求方式一:requests.request(method, url, **kwargs)
import requests
url = 'http://127.0.0.1:5000/hello'
data = {'username' : 'zs' , 'password' : 123 }
ret = requests.request('get' , url=url)
print (ret.json())
请求方式二:requests.get(url, params=None, **kwargs)
ret = requests.get(url=url, params=data)
print (ret.json())
发送 post 请求
请求方式一:requests.request(method, url, **kwargs)
ret = requests.request('post' , url=url, json=data)
ret = requests.request('post' , url=url, data=data)
print (ret.json())
请求方式二:request.post(url, data=None, json=None, **kwargs)
ret = requests.post(url=url, json=data)
ret = requests.post(url=url, data=data)
print (ret.json())
json 要求传字典,json.loads 将字符串转为字典,content-type 为 application/json
data 要求字符串,json.dumps 将字典转为字符串,content-type 不是 json 一般为 application/x-www-form-urlencoded
data 可以传字典等格式数据,只是会自动编码为字符串,如果传的本身是字符串,则不进行编码
想在 url 地址栏增加一些参数,可以指定 params 参数
params = {'page' : 2 , 'len' : 10 }
requests.post(url=url, json=data, params=params)
ret = requests.post(url=url, files={'image' : open ('a.jpg' , 'rb' )})
使用 session 发送请求 requests 发送请求是独立的,但是有时需要保存会话内容
session = requests.session()
ret = session.post(url=url, json=data)
ret = session.post(url=url, json=data)
如果使用 cookie,session 鉴权,使用 requests.session 请求
如果使用 token 鉴权,使用 requests 请求,手动使用 token 作为参数或者 header 传递
jsonpath 使用 jsonpath 用于在 json 数据中定位提取数据
使用前需要先安装:pip install jsonpath
$ 根节点
. 子节点
.. 所有符合条件
[index] 下标
[,] 多个结果选择
[start:end] 指定范围
?(@.property==value) 过滤
@ 当前节点
import jsonpath
jsonpath.jsonpath(response, '$.name' )
jsonpath.jsonpath(response, '$..title' )
jsonpath.jsonpath(response, '$.names[0]' )
jsonpath.jsonpath(response, '$.names[*]' )
jsonpath.jsonpath(response, '$.books[0,3]' )
jsonpath.jsonpath(response, "$.store.book[?(@.category == 'fiction')]" )
框架基本封装过程
目录介绍
cases 用例目录:存放 yaml 或者 excel 用例
parse:读取 yaml 或 excel 用例
core:核心执行器执行用例,本质是 pytest 执行
utils:工具类和常用方法封装
yaml 用例
基本格式 使用前安装:pip install pyyaml
格式:
缩进表示层级关系
大小写敏感
字符串直接写或者使用引号包裹
base_config:
case_type: 'ApiCase'
case_name: 'login'
case_module: '教务系统'
author: 'hlk'
test_steps:
- 登录接口:
request_type: send_post
url: 'http://127.0.0.1'
params:
s: /hello
pageIndex: 2
data_type: json
request_data:
username: hlk
password: 123
读取 yaml import yaml
def read_yaml (file_path ):
data = []
with open (file_path, 'r' , encoding='utf-8' ) as file:
data.append(yaml.safe_load(file))
return data
封装请求关键字 关键字类中存放一些方法,如发送请求,解析响应等关键字方法
class MyRequest :
def __init__ (self, request ):
self .request = request
def send_post (self, **kwargs ):
url = kwargs.get('url' , None )
params = kwargs.get('params' , None )
headers = kwargs.get('headers' , None )
files = kwargs.get('files' , None )
data = kwargs.get('data' , None )
request_data = {'url' : url, 'params' : params, 'headers' : headers, 'files' : files}
if kwargs.get('data_type' ) == 'json' :
request_data['json' ] = data
if kwargs.get('data_type' ) == 'data' :
request_data['data' ] = data
try :
ret = self .request.request('post' , **request_data)
except Exception as e:
print ('请求异常:' , e)
return ret
基本核心执行器 import pytest, requests
from api_frame.HAT.parse.read_yml import read_yaml
from api_frame.HAT.utils.my_request import MyRequest
class TestRunner :
case_info = read_yaml('../../test_cases/yml_cases/login.yml' )
@pytest.mark.parametrize('case_info' , case_info )
def test_excute_case (self, case_info ):
my_request = MyRequest(requests)
base_config = case_info.get('base_config' , {})
test_steps = case_info.get('test_steps' , [])
for test_step in test_steps:
(step_name, step_value), = test_step.items()
request_type = step_value.get('request_type' )
try :
request_func = my_request.__getattribute__(request_type)
except Exception as e:
print ('my_request 中没有找到对应的方法' , e)
ret = request_func(**step_value).json()
print (ret)
if __name__ == '__main__' :
pytest.main(['-vs' , __file__])
main 执行入口 创建 main 文件,在 main 文件中执行用例生成 allure 报告
安装配置 allure:
安装:pip install allure-pytest==2.13.5
下载 allure 压缩包并解压
将 bin 目录配置环境变量
allure --version 验证是否安装及配置好环境变量
本地打开报告需要安装 allure-combine
import pytest, os
from allure_combine import combine_allure
pytest_args = ['-vs' , '--capture=sys' , '--clean-alluredir' , '--alluredir=allure-results' , './core/test_runner.py' ]
pytest.main(pytest_args)
os.system('allure generate allure-results -o allure-report -c' )
combine_allure('./allure-report' )
核心执行器优化
优化报告 base_config = case_info.get('base_config' , {})
allure.dynamic.parameter('case_info' , '' )
allure.dynamic.feature(base_config.get('case_module' ))
allure.dynamic.story(base_config.get('case_module_sec' ))
allure.dynamic.title(base_config.get('case_name' ))
with allure.step(step_name),在函数中添加步骤
with allure.step(step_name):
request_type = step_value.get('request_type' )
try :
request_func = my_request.__getattribute__(request_type)
except Exception as e:
print ('my_request 中没有找到对应的方法' , e)
ret = request_func(**step_value).json()
print (ret)
@allure.step('发送 post 请求'),在函数外添加步骤
@allure.step('发送 post 请求' )
def send_post (self, **kwargs ):
url = kwargs.get('url' , None )
params = kwargs.get('params' , None )
headers = kwargs.get('headers' , None )
files = kwargs.get('files' , None )
data = kwargs.get('data' , None )
需要安装:pip install tqdm 进度条库,终端输出工具
注意导包:from tqdm import tqdm
test_steps = case_info.get('test_steps' , [])
with tqdm(total=len (test_steps), desc='开始执行' ) as pbar:
for test_step in test_steps:
(step_name, step_value), = test_step.items()
pbar.set_description(f'{base_config.get("case_name" )} -当前步骤:{step_name} ' )
pbar.update(1 )
为什么 tqdm 进度条会出现在 Allure 报告里?
tqdm 默认输出到 sys.stderr(标准错误,如异常堆栈、进度提示)
pytest 会自动捕获测试过程中的 stderr/stdout 输出
allure-pytest 插件会把 pytest 捕获的日志自动 attach 到 Allure 报告中,作为 stderr 或 stdout 附件
增加全局配置
yaml 用例文件改造 一些全局配置如项目 url 地址可以单独存放在一个 yaml 文件中,创建 context.yml 保存这些数据
login_url: 'http://127.0.0.1:8080/login'
register_url: 'http://127.0.0.1:8080/register'
file_upload_url: 'http://127.0.0.1:8080/upload'
file_download_url: 'http://127.0.0.1:8080/download/a.png'
databases:
hlk_test_dev:
user: 'root'
database: 'hlk_test_dev'
password: '12345678'
host: 'localhost'
port: 3306
session_reuse: True
在用例 yaml 中使用这些配置,可以使用{{变量}}
test_steps:
- 添加接口:
request_type: send_post
url: '{{login_url}}'
data_type: json
request_data:
username: hlk
password: 123
token: '{{token}}'
多个 yaml 之间数据传递思路:先将配置数据保存到全局变量中,再将全局变量渲染到 yaml 用例中
全局变量类
属性_dict 为类属性,每个实例共用一份
可以给实例方法上添加@classmethod,保证是类方法,里面修改会修改类属性,否则使用=进行 self.attr = value 赋值时,会优先在实例级别创建一个新的属性 attr,而不会修改类属性
可以使用类名。类属性进行修改如 GlobalVar._dict,而非 self._dict=value
如果实例方法中没有进行=号赋值,直接使用类属性则会先查找实例属性_dict,找不到再查找类属性_dict
class GlobalVar :
_dict = {}
def set_dict (self, dict ):
self ._dict .update(dict )
def set_dict_by_kv (self, key, value ):
self ._dict [key] = value
def get_dict (self ):
return self ._dict
def get_dict_by_key (self, key ):
return self ._dict .get(key)
global_var = read_context('./test_cases/yaml_cases/context.yml' )
GlobalVar().set_dict(global_var)
使用 jinjia2 进行渲染,需要安装:pip install jinja2
from jinja2 import Template
step_value = eval (Template(str (step_value)).render(GlobalVar().get_dict()))
增加前置后置脚本执行 yml 用例中添加前置及后置操作,此处是直接执行代码方式,可优化为调方法执行
base_config:
case_type: 'ApiCase'
case_name: 'login'
case_story: '登录模块'
case_module: '教务系统'
author: 'hlk'
setup_script:
- "print('前置执行 1')"
- "print('前置执行 2')"
teardown_script:
- "print('后置执行 1')"
- "print('后置执行 2')"
test_steps:
- 登录接口:
request_type: send_post
url: '{{login_url}}'
data_type: json
request_data:
username: hlk
password: 123
setup_scripts = case_info.get('setup_script' )
if setup_scripts:
for setup_script in setup_scripts:
exec (setup_script, {'context' : GlobalVar().get_dict()})
teardown_scripts = case_info.get('teardown_script' )
if teardown_scripts:
for teardown_script in teardown_scripts:
exec (teardown_script, {'context' : GlobalVar().get_dict()})
exec 函数:动态执行代码
exec(表达式,全局变量作用域,局部变量作用域),如果指定了局部变量作用域,则表达式生成的变量将会存在这里,不会污染全局作用域变量
多用例执行 执行多个用例:
按照特定规则读取指定目录下的 yml 文件
def read_rule_yaml (file_dir ):
case_infos = []
files = os.listdir(file_dir)
rule_files = [file for file in files if file.endswith('yml' ) and file.split('.' )[0 ].endswith('test' )]
rule_files.sort()
for rule_file in rule_files:
file = os.path.join(file_dir, rule_file)
with open (file, 'r' , encoding='utf-8' ) as f:
case_info = yaml.safe_load(f)
case_infos.append(case_info)
return case_infos
case_info = read_rule_yaml('../test_cases/yaml_cases/' )
用例之间进行数据关联 每次请求可以将响应数据保存到全局变量中,发送请求的方法增加保存响应数据到全局变量:
response = self .request.request('post' , **request_data)
GlobalVar().set_dict({'response_data' : response.json()})
从全局变量中拿到响应数据,再根据 yml 用例中定义的提取变量的规则从响应中提取到字段再保存到全局变量中
test_steps:
- 登录接口:
request_type: send_post
url: '{{login_url}}'
data_type: json
request_data:
username: hlk
password: 123
- 提取保存数据:
request_type: extract_data
extract_expression: '$..token'
extract_var: 'token'
@allure.step('提取保存数据' )
def extract_data (self, **kwargs ):
index = kwargs.get('extract_index' , None )
if index == None :
index = 0
response = GlobalVar().get_dict_by_key('response_data' )
extract_expression = kwargs.get('extract_expression' , None )
extract_var = kwargs.get('extract_var' , None )
extract_value = jsonpath.jsonpath(response, extract_expression)[index]
if not extract_value:
raise Exception(f'没有从响应中取到对应值{extract_expression} ' )
GlobalVar().set_dict_by_kv(extract_var, extract_value)
print (f'提取{extract_var} 的值{extract_value} ' )
下一个用例执行时,从全局变量中取需要的字段,之前的核心执行器中已经支持从全局变量渲染用例文件,所以此处只需使用{{}}替换变量值
step_value = eval (Template(str (step_value)).render(GlobalVar().get_dict()))
- 删除接口:
request_type: send_post
url: '{{register_url}}'
data_type: json
request_data:
username: hlk
password: 123
token: '{{token}}'
断言操作
单个字段进行断言 - 断言数据:
request_type: validate_data
validate:
- ==:
actual: '{{msg}}'
expect: good
assert_fail_reason: '实际与预期不相等'
- in:
actual: '{{msg}}'
expect: 'goods'
@allure.step('断言数据' )
def validate_data (self, **kwargs ):
comparators = {
'==' : lambda x, y: x == y,
'!=' : lambda x, y: x != y,
'>=' : lambda x, y: x >= y,
'<=' : lambda x, y: x <= y,
'in' : lambda x, y: x in y,
'not in' : lambda x, y: x not in y,
}
validates = kwargs.get('validate' )
for validate in validates:
(comparator, validate_data), = validate.items()
if comparator not in comparators:
raise Exception(f'{comparator} 比较符不在比较器中' )
expect = validate_data.get('expect' )
actual = validate_data.get('actual' )
assert_fail_reason = validate_data.get('assert_fail_reason' , None )
if not comparators[comparator](expect, actual):
if assert_fail_reason:
raise AssertionError(assert_fail_reason)
else :
raise AssertionError(f'{actual} 与 {expect} 不相等' )
整个响应结果进行断言 需要使用 deepdiff,安装:pip install deepdiff
可以设置忽略对比的字段,忽略大小写,忽略列表元素的顺序等
- 断言全部数据:
request_type: validate_all_data
expect:
code: 20
msg: 'goosd'
data:
token: 'esjdksaos'
name: 'zs'
uid: '44'
auths: ['prod' , 'dev' ]
filter_fields: ["root['data']['uid']" , "root['data']['token']" ]
@allure.step('断言全部响应数据' )
def validate_all_data (self, **kwargs ):
try :
actual_response = GlobalVar.get_dict_by_key('response_data' )
expect_response = kwargs.get('expect' )
filter_fields = kwargs.get('filter_fields' , [])
ignore_order = kwargs.get('ignore_order' , True )
ignore_case = kwargs.get('ignore_case' , True )
data = {
'exclude_paths' : filter_fields,
'ignore_order' : ignore_order,
'ignore_string_case' : ignore_case
}
diff = DeepDiff(actual_response, expect_response, **data)
except Exception as e:
assertFalse, f'批量断言失败:{e} '
assert not diff, f'批量断言失败{diff} '
数据库断言
链接数据库
创建游标
游标执行 sql
处理结果
关闭游标和链接
使用前需安装:pip install pymysql
import pymysql
from pymysql import cursors
connect = pymysql.connect(
user='root' ,
database='hlk_test_dev' ,
password='12345678' ,
host='localhost' ,
port=3306 ,
cursorclass=cursors.DictCursor,
charset='utf8'
)
cursor = connect.cursor()
sql = 'select * from user'
cursor.execute(sql)
ret = cursor.fetchall()
print (ret)
cursor.close()
connect.close()
yml 用例中先提取查询的数据库结果,再进行数据库断言
- 提取数据库数据:
request_type: extract_database
database_name: hlk_test_dev
sql: select * from user
extract_var: [username , password ]
- 断言数据库:
request_type: validate_data
validate:
- in:
actual: ''' {{username}}'' '
expect: 'zs'
assert_fail_reason: '实际与预期不相等'
数据库配置放在全局配置文件 context.yml 中
databases:
hlk_test_dev:
user: 'root'
database: 'hlk_test_dev'
password: '12345678'
host: 'localhost'
port: 3306
@allure.step('提取数据库数据' )
def extract_database (self, **kwargs ):
database_name = kwargs.get('database_name' )
db_config = GlobalVar().get_dict_by_key('databases' ).get(database_name)
config = {'cursorclass' : cursors.DictCursor, 'charset' : 'utf8' }
config.update(db_config)
connect = pymysql.connect(**config)
cursor = connect.cursor()
cursor.execute(kwargs.get('sql' ))
ret = cursor.fetchall()
cursor.close()
connect.close()
result = {}
vars = kwargs.get('extract_var' )
for var in vars :
if len (ret) <= 1 :
result[var] = ''
else :
result[var] = []
for i in ret:
for key, value in i.items():
if key == var:
if len (ret) <= 1 :
result[var] = value
else :
result[var].append(value)
GlobalVar().set_dict(result)
yml 数据驱动
用例文件的参数进行修改,使用花括号
ddt 数据添加到用例中
test_steps:
- 登录接口:
request_type: send_post
url: '{{login_url}}'
data_type: json
request_data:
username: '{{username}}'
password: '{{password}}'
- 提取响应数据 msg:
request_type: extract_data
extract_expression: '$..msg'
extract_var: 'msg'
- 断言数据:
request_type: validate_data
validate:
- ==:
actual: '{{msg}}'
expect: '{{expect}}'
assert_fail_reason: '实际与预期不相等'
ddt:
- title: '正确用户名'
username: hlk
password: 123
expect: 'login success'
- title: '错误用户名'
username: zs
password: 456
expect: 'login fail'
遍历每一个数据驱动,与原数据结构进行拼接(需新增)
执行一条用例前,先将数据驱动添加到全局变量中(需新增)
使用全局变量渲染组装好的用例数据(上述已经存在该操作)
def ddt_yaml_parse (file_dir ):
case_infos = read_rule_yaml(file_dir)
new_case_infos = []
for case_info in case_infos:
ddts = case_info.get('ddt' )
if ddts:
case_info.pop('ddt' )
for ddt in ddts:
case_info = copy.deepcopy(case_info)
case_info.update({'ddt' : ddt})
new_case_infos.append((case_info))
else :
new_case_infos.append(case_info)
return new_case_infos
核心执行器执行时会将全局变量渲染到用例文件
core 核心执行器中,增加将数据驱动保存到全局变量:
GlobalVar().set_dict(case_info.get('ddt' ))
allure 与 loguru 日志
基本使用 安装:pip install loguru
基本使用:
from loguru import logger
logger.add('log.log' ,
rotation='100 MB' ,
retention='10 days' )
logger.debug('调试级别' )
logger.info('程序正常运行' )
logger.warning('警告信息' )
logger.error('错误信息' )
try :
a = 1 /0
logger.info('run...' )
except Exception as e:
logger.error(e)
logger.add 在系统日志配置默认基础上增加
logger.configure(handlers=None),handlers 一个列表,列表中的每一项是一个字典,用于定义一个日志输出 sink,重新配置整个 logger
time_str = datetime.now().strftime("%Y-%m-%d_%H:%M:%S" )
logger.configure(handlers=[
{"sink" : sys.stdout, "level" : 'DEBUG' },
{"sink" : sys.stderr, "level" : 'DEBUG' },
{"sink" : f'./log/log_{time_str} .log' , "level" : 'DEBUG' }
])
if not os.path.exists('./log' ):
os.mkdir('log' )
time_str = datetime.now().strftime("%Y-%m-%d_%H:%M:%S" )
logger.configure(handlers=[
{"sink" : sys.stdout, "level" : 'DEBUG' },
{"sink" : sys.stderr, "level" : 'DEBUG' },
{"sink" : f'./log/log_{time_str} .log' , "level" : 'DEBUG' }
])
创建日志管理器关联步骤日志 步骤与日志关联,日志信息放在每个 allure 测试步骤下面:
import io
import allure
from loguru import logger
class AllureStepLogger ():
def __init__ (self ):
self .log_buffer = io.StringIO()
self .sink_id = None
def __enter__ (self ):
self .sink_id = logger.add(self .log_buffer, level='DEBUG' )
def __exit__ (self, exc_type, exc_val, exc_tb ):
logger.remove(self .sink_id)
log = self .log_buffer.getvalue()
if log.strip():
allure.attach(
body=log,
name='步骤日志' ,
attachment_type=allure.attachment_type.TEXT
)
self .log_buffer.close()
from contextlib import contextmanager
@contextmanager
def allure_step_with_log (step_name ):
with allure.step(step_name):
with AllureStepLogger() as collector:
yield collector
with allure_step_with_log(step_name):
调用方执行 with allure_step_with_log(step_name)
进入 allure_step_with_log 函数(因为有@contextmanager,先执行 yield 之前相当于__enter__)
执行 with allure.step(step_name)
执行 with AllureStepLogger() as collector
执行 AllureStepLogger 的__enter__方法
遇到 yield,将控制权交给调用方,执行调用方 with allure_step_with_log(step_name) 里的代码
执行 yield 后的方法,相当于__exit__
退出 with AllureStepLogger() as collector 语句块
退出 with allure.step(step_name) 语句块
退出函数 allure_step_with_log(step_name)
session 复用机制 在 context.yml 中可以配置 session 是否复用
global_session = None
class ApiCaseContext :
def __init__ (self ):
self .request = None
self .my_request = None
def init_kwargs_func (self ):
global global_session
session_reuse = GlobalContext().get_dict_by_key('session_reuse' )
if session_reuse is not None and session_reuse == True :
if global_session == None :
global_session = requests.session()
self .request = global_session
else :
self .request = requests
self .my_request = MyRequest(self .request)
return self .my_request
my_request = ApiCaseContext().init_kwargs_func()
文件上传与下载
文件上传 test_steps:
- 文件上传接口:
request_type: send_post
url: '{{file_upload_url}}'
data_type: files
request_data:
img: 'a.png'
if data_type == 'files' :
if isinstance (data, dict ):
files = {}
for key, file_path in data.items():
files[key] = open (file_path, 'rb' )
request_data['files' ] = files
else :
file = open (data, 'rb' )
request_data['files' ] = {'files' : file}
文件下载 - 文件下载接口:
request_type: send_get
url: '{{file_download_url}}'
save_path: '/Users/user/Desktop/hlk_test_dev/requests_test/HAT/utils/download/b.png'
stream: True
chunk_size: 1024
@allure.step('发送 get 请求' )
def send_get (self, **kwargs ):
url = kwargs.get('url' )
params = kwargs.get('params' )
data_type = kwargs.get('data_type' )
data = kwargs.get('request_data' )
save_path = kwargs.get('save_path' )
stream = kwargs.get('stream' , False )
chunk_size = kwargs.get('chunk_size' , 1024 )
request_data = {'url' : url, 'params' : params, 'stream' : stream if save_path else False ,}
if data_type == 'json' :
request_data['json' ] = data
if data_type == 'data' :
request_data['data' ] = data
try :
response = self .request.request('get' , **request_data)
logger.debug('发送请求 success' )
if save_path:
os.makedirs(os.path.dirname(save_path), exist_ok=True )
if stream:
with open (save_path, 'wb' ) as file:
for chunk in response.iter_content(chunk_size=chunk_size):
file.write(chunk)
else :
with open (save_path, 'wb' ) as file:
file.write(response.content)
except Exception as e:
print ('请求错误:' , e)
return response
pytest 自定义插件 给 pytest 添加两个自定义执行参数,case(用例目录),type(用例格式 yml 或 excel),
pytest 原本不支持该参数,现在扩展支持让 pytest 支持 case 目录参数,type 用例格式参数
修改执行入口 main 文件,添加自定义执行参数与插件
pytest_args = ['-vs' , '--capture=sys' , '--clean-alluredir' , '--alluredir=allure-results' , './core/core.py' ,
'--case=../test_cases/yaml_cases_01' ,
'--type=yml'
pytest.main(pytest_args, plugins=[CasePlugin()])
创建自定义插件类:
将钩子函数定义在插件中,pytest 运行时会扫描加载的插件和测试代码,寻找这些钩子函数,匹配到就会在合适的时机调用它,这里使用到的钩子函数:
pytest_addoption,用于向 pytest 的命令行添加自定义参数
pytest_generate_tests,用于动态生成测试参数
pytest_collection_modifyitems,用于在收集测试用例(还未开始执行)进行修改或操作如如更改名称,跳过某些用例,重新排序
import pytest
from requests_test.HAT.parse.yaml_parse import parse_pytest_parameter
class CasePlugin :
def pytest_addoption (self, parser ):
parser.addoption('--case' ,
action='store' ,
default='' ,
help ='用例目录' )
parser.addoption('--type' ,
action='store' ,
default='' ,
help ='用例格式类型,yml 还是 excel' )
def pytest_generate_tests (self, metafunc ):
case = metafunc.config.getoption('case' )
type = metafunc.config.getoption('type' )
case_infos, case_names = parse_pytest_parameter(case , type )
if 'case_info' in metafunc.fixturenames:
metafunc.parametrize('case_info' , case_infos, ids=case_names)
def pytest_collection_modifyitems (self, items ):
for item in items:
item.name = item.name.encode('utf-8' ).decode('unicode_escape' )
item._nodeid = item._nodeid.encode('utf-8' ).decode('unicode_escape' )
item.add_marker(pytest.mark.slow)
if 'skip' in item.name:
item.add_marker(pytest.mark.skip(reason='skip this' ))
最后再注释掉核心执行器中使用@pytest.mark.parametrize 进行用例参数化的代码
打包
提高复用性:封装框架为模块,方便在其他项目中使用。
方便分发:通过打包和上传到 PyPI,方便团队和用户安装。
便于部署:简化框架的部署流程,支持标准化安装。
管理依赖:明确框架的依赖项,自动安装所需的库。
支持版本管理:通过版本号管理框架的更新和兼容性。
提升专业性:增强框架的可用性和标准化。
集成到 CI/CD:支持自动化构建、测试和发布。
工作目录可能会不同,使用的相对路径,Python 会以当前工作目录为基准来解析路径
右键运行:默认的工作目录通常是项目的根目录,可在 pycharm 的 Working directory 中手动设置工作目录
命令运行:工作目录是运行命令所在的终端目录
右键运行:通常会添加项目的根目录到 sys.path,以便导入项目内的模块
命令运行:会将当前执行文件的目录添加到 sys.path[0]
故命令执行与右键执行,工作目录与导包路径可能不同,导致同一份代码 可能不同时支持右键和命令运行
将框架打包,支持终端运行,打包运行命令的工作目录和导包路径都是当前运行命令的终端路径
main 文件内容替换
python 获取命令执行参数# import sys
args = sys.argv
import argparse
parser = argparse.ArgumentParser(description='入口执行参数' )
parser.add_argument('-a' , help ='参数 a' , type =int , default=0 )
parser.add_argument('b' , help ='参数 b' , type =str )
parser.add_argument('-c' , '--chlp' , help ='参数 b' , type =str , default='name' )
args = parser.parse_args()
print (args.a)
print (args.b)
print (args.chlp)
main 文件内容 from loguru import logger
import sys
from datetime import datetime
import pytest, os, argparse
from HAT.core import core
from HAT.core.case_plugins import CasePlugin
from _pytest.config import ExitCode
import subprocess
if not os.path.exists('HAT/log' ):
os.mkdir('HAT/log' )
time_str = datetime.now().strftime("%Y-%m-%d_%H:%M:%S" )
logger.configure(handlers=[
{"sink" : sys.stdout, "level" : 'DEBUG' },
{"sink" : sys.stderr, "level" : 'DEBUG' },
{"sink" : f'./log/log_{time_str} .log' , "level" : 'DEBUG' }
])
def args_parser ():
parser = argparse.ArgumentParser(description='接口测试工具' )
parser.add_argument('--type' , type =str , default='yml' , help ='用例文件类型' )
parser.add_argument('--case' , type =str , default='./test_cases/yaml_cases_01' , help ='指定测试用例的文件夹路径.' )
parser.add_argument('--alluredir' , type =str , default=os.path.join(os.getcwd(), "allure_results" ), help ='运行结果数据文件路径' )
parser.add_argument('--allure_report' , type =str , default=os.path.join(os.getcwd(), "allure_report" ), help ='测试报告保存路径' )
args = parser.parse_args()
return args
cmd_args = args_parser()
def run ():
pytest_args = ['-vs' , '--capture=sys' , '--clean-alluredir' ]
if cmd_args.type :
pytest_args.append(f'--type={cmd_args.type } ' )
if cmd_args.case :
pytest_args.append(f'--case={cmd_args.case } ' )
if cmd_args.alluredir:
pytest_args.append(f'--alluredir={cmd_args.alluredir} ' )
pytest_args.append(core.__file__)
logger.info('pytest 开始执行用例' )
exit_code = pytest.main(pytest_args, plugins=[CasePlugin()])
if exit_code == ExitCode.OK or exit_code == ExitCode.TESTS_FAILED:
try :
subprocess.run(f'allure generate {cmd_args.alluredir} -o {cmd_args.allure_report} --clean' , shell=True , check=True , universal_newlines=True )
except subprocess.CalledProcessError as e:
logger.error(f"测试报告出现问题!{e} " )
else :
logger.error('pytest 执行异常' )
if __name__ == '__main__' :
run()
导出框架依赖包
导出,pip freeze > requirement.txt
安装,pip install -r requirement.txt
增加 setup 文件 setup.py 是 Python 项目打包和分发的核心配置文件,用于定义项目的元数据、依赖项和安装配置等内容,通过 setup.py,我们可以将项目打包成标准的 Python 包 方便分发和安装
setuptools 是 Python 中一个强大的库,用于打包和分发 Python 项目
import setuptools
""" 打包成一个 可执行模块 """
long_description = '自动化测试工具'
setuptools.setup(
name="apiPlatform" ,
version="1.0.1" ,
author="author" ,
author_email="[email protected] " ,
description="自动化测试工具" ,
license="GPLv3" ,
long_description=long_description,
long_description_content_type="text/markdown" ,
classifiers=[
"Programming Language :: Python :: 3" ,
"License :: OSI Approved :: GNU General Public License (GPL)" ,
"Operating System :: OS Independent" ,
],
install_requires=[
"pytest==8.4.2" ,
"Jinja2==3.1.6" ,
"jsonpath==0.82.2" ,
],
py_modules=["main" ],
packages=setuptools.find_packages(),
python_requires=">=3.8" ,
entry_points={
'console_scripts' : [
'tc-api-test=main:run'
]
},
zip_safe=False
)
创建文件后,执行 python setup.py install,然后就支持上述命令执行
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 curl 转代码 解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online