Pytest+requests进行接口自动化测试8.0(Allure进阶 + 文件上传接口 + 单接口多用例)
Allure进阶
- 一、Allure进阶
- 1. 标题后面的参数隐藏及标题动态展示
- 1)在接口对应的 py 文件中添加 allure 标题展示
- 2)修改 allure 里的 listener.py 文件
- 2. 显示 allure 报告的用例顺序
- 1)模块编号方法
- 2)用例编号方法
- 3)模块及用例编号方法使用
- 二、pytest进阶
- 1. 文件上传接口
- 2. 单接口多条测试用例
- 1)前置条件
- 2)分别读取YAML文件 baseInfo,test_case
- 3)拆分使用 baseInfo,test_case
- 编号生成器
- 4)修改接口文件
一、Allure进阶
1. 标题后面的参数隐藏及标题动态展示
目标:将yaml文件中的测试用例名动态展示到 allure 中
- 未修改前
yaml 文件内容为
test_case:- case_name: 用户名和密码正确登录验证json:username: "{{get_data(username, login_data.csv)}}" # 注意:传了 filenamepassword: "{{get_data(password, login_data.csv)}}"validation:- contains: {code: "200"}- contains: {message: "成功"}extract:token: '"token":"(.*?)"'
1)在接口对应的 py 文件中添加 allure 标题展示
选择 allure.dynamic.title
代码展示
import pytest
import allure
from common.run_yaml import ReadYamlData
from conf.setting import FILE_PATH
from base.apiutil import BaseRequest@allure.feature("登录接口")
class TestLogin:readyaml = ReadYamlData()@allure.story("正确登录")@pytest.mark.parametrize('case_info', readyaml.read(FILE_PATH['login']))def test_case01(self, case_info):allure.dynamic.title(case_info['test_case'][0]['case_name'])BaseRequest().specification_yaml(case_info)
修改之后,标题展示了,但后面参数占位,需隐藏参数
2)修改 allure 里的 listener.py 文件
若找不到文件位置,cmd输入以下语句
cd C:\
dir /s listener.py
找到文件位置
双击打开文件
文件中查找 test_result.parameters.extend 此处为参数展示
将内容置空,变为 [ ] 空列表
再次运行成功隐藏参数并正确展示 yaml 标题
2. 显示 allure 报告的用例顺序
最终效果
1)模块编号方法
目标: 生成一系列格式为 M01_, M02_, M03_, …, M99_ 的“模块编号”
在 base 下创建 generateId.py 文件
编写生成模块编号方法
def generate_module_id():"""生产测试模块编号,为了保证allure报告的顺序与pytest设定的执行顺序保持一致:return:"""for i in range(1,100):module_id = 'M' + str(i).zfill(2) + '_' # zfill(2) 表示将数字补零到 2 位,例如:1 → '01', 10 → '10'yield module_id # yield可以按序生成,return每次只生成一个m_id = generate_module_id()
- zfill(2) 表示将数字补零到 2 位,例如:1 → ‘01’, 10 → ‘10’
- yield可以按序生成,return每次只生成一个
结果展示:
2)用例编号方法
目标: 生成测试用例编号,格式为 C01_, C02_, …, C99_
在 generateId.py 文件中继续编写方法
编写生成用例编号方法
def generate_testcase_id():"""生成测试用例编号:return:"""for i in range(1,100):case_id = 'C' + str(i).zfill(2) + '_'yield case_idc_id = generate_testcase_id()
结果展示:
3)模块及用例编号方法使用
在接口运行文件中使用 c_id ,m_id 方法
引入 base.generateId 中的 c_id , m_id 方法
from base.generateId import c_id,m_id
使用模块编号
@allure.feature(next(m_id) + "登录接口")
使用用例编号
@allure.story(next(c_id) + "正确登录")
运行文件中整体代码展示
import pytest
import allure
from common.run_yaml import ReadYamlData
from conf.setting import FILE_PATH
from base.apiutil import BaseRequest
from base.generateId import c_id,m_id@allure.feature(next(m_id) + "登录接口")
class TestLogin:readyaml = ReadYamlData()@allure.story(next(c_id) + "正确登录")@pytest.mark.parametrize('case_info', readyaml.read(FILE_PATH['login']))def test_case01(self, case_info):allure.dynamic.title(case_info['test_case'][0]['case_name'])BaseRequest().specification_yaml(case_info)@allure.story(next(c_id) + "错误登录")@pytest.mark.parametrize('case_info', readyaml.read(FILE_PATH['login']))def test_case02(self, case_info):allure.dynamic.title(case_info['test_case'][0]['case_name'])BaseRequest().specification_yaml(case_info)
最终结果展示:
二、pytest进阶
1. 文件上传接口
注意:文件上传接口一般不需要请求头
testCase:- case_name:导入车辆黑名单文件files:file: ./data/heimingdan online.xlsx
在 apiutil.py 文件 读取yaml测试用例方法 新增内容 - - - 以二进制方式读取文件内容
# 处理文件上传接口file,files = to.pop('files',None),None # to为yaml数据 test_case 的拆分if file is not None:for fk,fv in file.items():allure.attach(json.dumps(file), '导入文件')files = {fk:open(fv,mode='rb')} # 以二进制的方式读取文件内容res = self.send.run_main(name=api_name, url=url, case_name=case_name,method=method, headers=headers, cookies=cookie,file=files, **to)
2. 单接口多条测试用例
1)前置条件
yaml 文件,单接口包含多条测试用例
- baseInfo:api_name: 用户登录url: /api/basic/auth/loginWithoutCheckCodemethod: POSTheader:Content-Type: application/jsoncookies:session: hjsksksjj17272test_case:- case_name: 用户名和密码正确登录验证json:username: "{{get_data(username, login_data.csv)}}" # 注意:传了 filenamepassword: "{{get_data(password, login_data.csv)}}"validation:- contains: {code: "200"}- contains: {message: "成功"}extract:token: '"token":"(.*?)"'- case_name: 用户名和密码异常登录验证json:username: "{{get_data(username, login_data.csv)}}" # 注意:传了 filenamepassword: "{{get_data(password, login_data.csv)}}"validation:- contains: { code: "200" }- contains: { message: "成功" }extract:token: '"token":"(.*?)"'
2)分别读取YAML文件 baseInfo,test_case
避免重复写 URL、header、method 等公共信息,提高 YAML 可维护性
将yaml文件的 baseInfo,test_case 分别读取出来
def read(self, yaml_file):"""读取 YAML 文件,返回 [(base_info, test_case), ...] 列表用于参数化"""if not os.path.exists(yaml_file):raise FileNotFoundError(f"配置文件不存在: {yaml_file}")with open(yaml_file, 'r', encoding='utf-8') as f:try:datas = yaml.safe_load(f)if not datas:raise ValueError("YAML 文件为空")# 假设 datas 是一个列表,每个元素是一个接口testcase_list = []for item in datas:base_info = item.get('baseInfo')test_cases = item.get('test_case') # 注意:与 YAML 一致if not base_info or not test_cases:raise ValueError(f"YAML 格式错误:缺少 baseInfo 或 test_case")for case in test_cases:testcase_list.append((base_info, case))return testcase_listexcept yaml.YAMLError as e:raise ValueError(f"YAML 解析错误: {e}")except Exception as e:raise ValueError(f"读取 YAML 失败: {e}")
3)拆分使用 baseInfo,test_case
在单接口运行文件中添加 baseInfo,test_case 参数
@allure.feature(next(m_id) + "登录接口")
class TestLogin:readyaml = ReadYamlData()@allure.story(next(c_id) + "正确登录")@pytest.mark.parametrize('base_info,test_case', readyaml.read(FILE_PATH['login']))def test_case01(self, base_info,test_case):allure.dynamic.title(test_case['case_name'])BaseRequest().specification_yaml(base_info,test_case)
- @pytest.mark.parametrize 实现了“一接口多用例”的核心;
- allure.dynamic.title() 动态设置用例标题,避免所有用例都叫 test_case01
- next(m_id) 和 next(c_id) 是自定义的编号生成器
编号生成器
def generate_module_id():"""生产测试模块编号,为了保证allure报告的顺序与pytest设定的执行顺序保持一致:return:"""for i in range(1,100):module_id = 'M' + str(i).zfill(2) + '_' # zfill(2) 表示将数字补零到 2 位,例如:1 → '01', 10 → '10'yield module_id # yield可以按序生成,return每次只生成一个def generate_testcase_id():"""生成测试用例编号:return:"""for i in range(1,100):case_id = 'C' + str(i).zfill(2) + '_'yield case_idm_id = generate_module_id()
c_id = generate_testcase_id()
4)修改接口文件
修改接口文件 baseInfo,test_case 读取
def specification_yaml(self,base_info,test_case):"""接口请求处理基本方法:param base_info: yaml文件里的baseInfo:param test_case: yaml文件里的testCase:return:"""try:params_type = ['params', 'json', 'data']cookie = None"""获取接口的基础数据"""base_url = self.conf.get_envi('host')url = urljoin(f"{base_url}", base_info['url'])allure.attach(url, f'接口地址:{url}')api_name = base_info['api_name']allure.attach(api_name, f'接口名称:{api_name}')method = base_info['method']allure.attach(method, f'请求方法:{method}')# 先取出 headers,再替换headers = base_info['header']headers = self.replace_load(headers)allure.attach(str(headers), f'请求头:{headers}', allure.attachment_type.TEXT)try:cookie = base_info.get('cookies')if cookie:cookie = self.replace_load(cookie)cookie_str = json.dumps(cookie, ensure_ascii=False) if isinstance(cookie,dict) else str(cookie)allure.attach(cookie_str, 'Cookie信息', allure.attachment_type.TEXT)except Exception as e:logs.warning(f"处理cookie失败: {e}")"""获取该接口的所有测试用例"""case_name = test_case.pop('case_name') # 把case_name删除allure.attach(case_name, f'测试用例名称:{case_name}')# 处理断言val = self.replace_load(test_case.get('validation'))test_case['validation'] = valvalidation = test_case.pop('validation') # 把validation删除# 处理参数提取extract = test_case.pop('extract', None)extract_list = test_case.pop('extract_list', None) # 把extract_list删除# 处理接口的请求参数for key, value in test_case.items():if key in params_type:test_case[key] = self.replace_load(value) # 解析value# 处理文件上传接口file,files = test_case.pop('files',None),Noneif file is not None:for fk,fv in file.items():allure.attach(json.dumps(file), '导入文件')files = {fk:open(fv,mode='rb')} # 以二进制的方式读取文件内容res = self.send.run_main(name=api_name, url=url, case_name=case_name,method=method, headers=headers, cookies=cookie,file=files, **test_case)allure.attach(res.text, f'接口的响应信息:{res}', allure.attachment_type.TEXT)res_json = res.json()if extract is not None:self.extract_data(extract,res.text)if extract_list is not None:self.extract_data_list(extract_list,res.text)# 处理接口断言self.assert_res.assert_result(validation,res_json,res.status_code)except Exception as e:logs.error(e)raise e
- replace_load 是用于解析 {{get_data(…)}} 这类动态表达式的函数(如正则替换 + CSV 读取))