支持上传文件接口功能

This commit is contained in:
floraachy
2023-06-19 15:56:00 +08:00
parent 2f6aacfbc2
commit ec85f08e98
5 changed files with 319 additions and 91 deletions
+159 -1
View File
@@ -77,6 +77,7 @@
├────outputs/ ├────outputs/
│ └────report/ 保存测试报告的目录 │ └────report/ 保存测试报告的目录
│ └────log/ 保存日志文件的目录 │ └────log/ 保存日志文件的目录
├────files 存放测试过程中需要上传的文件
├────Pipfile ├────Pipfile
├────pytest.ini ├────pytest.ini
├────README.md ├────README.md
@@ -95,7 +96,6 @@
## 四、依赖库 ## 四、依赖库
``` ```
python_version = "3.9"
pymysql = "*" pymysql = "*"
loguru = "*" loguru = "*"
requests-toolbelt = "*" requests-toolbelt = "*"
@@ -112,6 +112,8 @@ pytest = "==6.2.5"
pytest-html = "==2.1.1" pytest-html = "==2.1.1"
pytest-rerunfailures = "*" pytest-rerunfailures = "*"
allure-pytest = "==2.9.45" allure-pytest = "==2.9.45"
pydantic = "*"
xpinyin = "*"
``` ```
@@ -475,6 +477,162 @@ wechat_content = """
``` ```
### 7. 上传文件接口支持
#### 熟悉接口
- 确定上传文件接口的URL
- 确定上传文件接口的METHOD
- 确定上传文件接口请求头里面的Content-Type, 我这边调试的接口都是:multipart/form-data;
- 确定上传文件接口请求参数, 我调试的接口有两种参数形式:
```python
# 第一种
file: 文件二进制内容
# 第二种
file: 文件二进制内容
language: zh
```
#### 文件上传的逻辑
`common_utils/base_request.py` 中封装的request请求,是使用`from requests_toolbelt import MultipartEncoder`进行文件上传的。
- 针对单文件上传,我们需要传递一个字典,参考如下:
```python
field = {
{
'file': (filename, file_content), # file是接口中文件参数的名称, filename是文件名,file_content是文件二进制内容
"key": v # 这里是文件上传的其他参数
}
}
```
- 针对多文件上传,我们需要传递一个列表嵌套元祖,参考如下:
```python
field =[
('file', (filename, file_content)), # file是接口中文件参数的名称, filename是文件名,file_content是文件二进制内容
('file', (filename, file_content)), # 这里是文件上传的其他参数
(k, v)
]
```
#### 上传文件,不带其他参数
- 我们需要设置: request_type=file
- 然后在files中按照如下格式书写:{接口中文件参数的名称:"文件路径地址"/["文件地址1", "文件地址2"]}
- 参考如下:
```yaml
# 公共参数
case_common:
allure_epic: GitLink接口(手动编写用例)
allure_feature: 上传文件模块
allure_story: 上传文件
# 用例数据
case_upload_demo_01:
feature: 上传文件
title: 测试单文件上传
run: True
url: /api/attachments.json
method: POST
headers:
cookies: ${login_cookie}
cookies:
request_type: file
payload:
files:
file: TOC出库订单导入模板(2).xlsx # 此处file对应接口中文件参数的名称
extract:
file_id: $.id
assert_response:
eq:
http_code: 200
assert_sql:
case_upload_demo_02:
feature: 上传文件
title: 测试多文件上传(该接口不支持多文件上传,这是一个示例)
run: False
url: /api/attachments.json
method: POST
headers:
cookies: ${login_cookie}
cookies:
request_type: file
payload:
files:
file:
- 导入TOC订单.xls
- toc.xls
extract:
file_id: $.id
assert_response:
eq:
http_code: 200
assert_sql:
```
#### 上传文件,带其他参数
- 我们需要设置: request_type=file
- 然后在files中按照如下格式书写:{接口中文件参数的名称:"文件路径地址"/["文件地址1", "文件地址2"]}
- 由于请求参数里面还传递了`language:zh` 因此我们需要写在`payload`
-`common_utils/base_request.py` 中,我们会将`language:zh`以元祖形式处理到files里面
- 参考如下:
```yaml
# 公共参数
case_common:
allure_epic: OWMS系统(自动生成用例) # 敏捷里面的概念,定义史诗,相当于module级的标签, 往下是 feature
allure_feature: 出库模块 # 功能点的描述,相当于class级的标签, 理解成模块往下是 story
allure_story: TOC扫描签出接口 # 故事,可以理解为场景,相当于method级的标签, 往下是 title
# 用例数据
case_import_toc_01:
feature: OMS系统
title: 导入TOC订单(01
run: True
url: /oms/retailGoodsTmp/selfImportExcel
method: POST
headers:
Cookietoken: ${oms_cookieToken}
request_type: file
payload:
language: zh
files:
file: TOC出库订单导入模板(2).xlsx
extract:
assert_response:
eq:
$.msg: 成功
assert_sql:
case_import_toc_02:
feature: OMS系统
title: 导入TOC订单(02
run: True
url: /oms/retailGoodsTmp/selfImportExcel
method: POST
headers:
Cookietoken: ${oms_cookieToken}
request_type: file
payload:
language: zh
files:
file:
- 导入TOC订单.xls
- toc.xls
extract:
assert_response:
eq:
$.msg: 成功
assert_sql:
```
## 初始化项目可能遇到的问题 ## 初始化项目可能遇到的问题
### 1. 测试机安装的是python3.7,但是本框架要求3.9.5,怎么办? ### 1. 测试机安装的是python3.7,但是本框架要求3.9.5,怎么办?
方法一:建议采纳此方法 方法一:建议采纳此方法
+66 -26
View File
@@ -6,6 +6,8 @@
# @Software: PyCharm # @Software: PyCharm
# @Desc: 处理request请求前后的用例数据 # @Desc: 处理request请求前后的用例数据
import json import json
import os.path
from common_utils.files_handle import get_file_field
from common_utils.data_handle import eval_data_process, data_replace from common_utils.data_handle import eval_data_process, data_replace
from config.global_vars import GLOBAL_VARS from config.global_vars import GLOBAL_VARS
from requests import Response from requests import Response
@@ -13,6 +15,7 @@ from loguru import logger
from common_utils.data_handle import json_extractor, re_extract from common_utils.data_handle import json_extractor, re_extract
from case_utils.allure_handle import allure_step from case_utils.allure_handle import allure_step
from common_utils.base_request import BaseRequest from common_utils.base_request import BaseRequest
from config.path_config import FILES_DIR
# ---------------------------------------- 请求前的数据处理----------------------------------------# # ---------------------------------------- 请求前的数据处理----------------------------------------#
@@ -107,29 +110,31 @@ class RequestPreDataHandle:
""" """
requests模块中,cookies参数要求是Dict or CookieJar object requests模块中,cookies参数要求是Dict or CookieJar object
""" """
cookies = self.request_data.get("cookies", None)
try: try:
# 从用例数据中获取cookies 处理cookies # 从用例数据中获取cookies 处理cookies
if self.request_data.get("cookies", None): if cookies:
# 通过全局变量替换cookies,得到的是一个str类型 # 通过全局变量替换cookies,得到的是一个str类型
cookies = data_replace(content=self.request_data.get("cookies"), source=GLOBAL_VARS) cookies = data_replace(content=cookies, source=GLOBAL_VARS)
if isinstance(cookies, str): if isinstance(cookies, str):
# 如果是字符串类型,就转成字典 # 如果是字符串类型,就转成字典
self.request_data["cookies"] = json.loads(cookies) self.request_data["cookies"] = json.loads(cookies)
else: else:
self.request_data["cookies"] = cookies self.request_data["cookies"] = cookies
except Exception as e: except Exception as e:
logger.error(f"处理cookies报错了:{e}") logger.error(f"处理{cookies}报错了:{e}")
print(f"处理cookies报错了:{e}") print(f"处理{cookies}报错了:{e}")
def headers_handle(self): def headers_handle(self):
""" """
headers里面传cookies,要求cookies类型是str headers里面传cookies,要求cookies类型是str
""" """
headers = self.request_data.get("headers", None)
try: try:
# 从用例数据中获取header 处理header # 从用例数据中获取header 处理header
if self.request_data.get("headers", None): if headers:
self.request_data["headers"] = eval_data_process( self.request_data["headers"] = eval_data_process(
data_replace(content=self.request_data.get("headers"), source=GLOBAL_VARS)) data_replace(content=headers, source=GLOBAL_VARS))
# 如果请求头中有cookies,需要进行单独处理 # 如果请求头中有cookies,需要进行单独处理
if self.request_data["headers"].get("cookies", None): if self.request_data["headers"].get("cookies", None):
cookies = self.request_data["headers"]["cookies"] cookies = self.request_data["headers"]["cookies"]
@@ -139,44 +144,75 @@ class RequestPreDataHandle:
else: else:
self.request_data["headers"]["cookies"] = cookies self.request_data["headers"]["cookies"] = cookies
except Exception as e: except Exception as e:
logger.error(f"处理header报错了:{e}") logger.error(f"处理{headers}报错了:{e}")
print(f"处理header报错了:{e}") print(f"处理{headers}报错了:{e}")
def payload_handle(self): def payload_handle(self):
# 处理请求参数payload
payload = self.request_data.get("payload", None)
try: try:
# 处理请求参数payload if payload:
if self.request_data.get("payload", None):
self.request_data["payload"] = eval_data_process( self.request_data["payload"] = eval_data_process(
data_replace(content=self.request_data.get("payload"), source=GLOBAL_VARS)) data_replace(content=payload, source=GLOBAL_VARS))
except Exception as e: except Exception as e:
logger.error(f"处理payload报错了:{e}") logger.error(f"处理{payload}报错了:{e}")
print(f"处理payload报错了:{e}") print(f"处理{payload}报错了:{e}")
def files_handle(self): def files_handle(self):
# 处理文件 """
# TODO 暂时还没想好怎么处理 格式:接口中文件参数的名称:"文件路径地址"/["文件地址1", "文件地址2"]
pass 例如:{"file": "test_demo.py"}
或者
{"file": ["test_demo_01.py", "test_demo_02.py"]}
"""
# 处理请求参数files参数
files = self.request_data.get("files", None)
try:
if files:
for k, v in files.items():
# ------------------ 处理多文件的情况 ------------------
# 这里需要注意:不一定所有接口都支持多文件上传
_files = []
if isinstance(v, list):
for file in v:
# 处理文件绝对路径
file_path = os.path.join(FILES_DIR, file)
# 多文件上传需要是元祖[('file', (filename, file_content)), ('file', (filename, file_content))]
_files.append((k, get_file_field(file_path)))
self.request_data["files"] = _files
else:
# ------------------ 处理单文件的情况 ------------------
# 处理文件绝对路径
file_path = os.path.join(FILES_DIR, v)
# 单文件上传需要是字典{'file': (filename, file_content)}
self.request_data["files"] = {k: get_file_field(file_path)}
logger.debug(f"处理完成后的file:{self.request_data['files']}")
except Exception as e:
logger.error(f"处理{files}报错了:{e}")
print(f"处理{files}报错了:{e}")
def extract_handle(self): def extract_handle(self):
# 处理后置提取参数
extract = self.request_data.get("extract", None)
try: try:
# 处理后置提取参数 if extract:
if self.request_data.get("extract", None):
# 仅提取参数中的python表达式,不需要进行数据替换 # 仅提取参数中的python表达式,不需要进行数据替换
self.request_data["extract"] = eval_data_process(self.request_data.get("extract")) self.request_data["extract"] = eval_data_process(extract)
except Exception as e: except Exception as e:
logger.error(f"处理extract报错了:{e}") logger.error(f"处理{extract}报错了:{e}")
print(f"处理extract报错了:{e}") print(f"处理{extract}报错了:{e}")
def assert_handle(self): def assert_handle(self):
# 处理响应断言参数
assert_response = self.request_data.get("assert_response", None)
try: try:
# 处理响应断言参数 if assert_response:
if self.request_data.get("assert_response", None):
self.request_data["assert_response"] = eval_data_process( self.request_data["assert_response"] = eval_data_process(
data_replace(content=self.request_data.get("assert_response"), source=GLOBAL_VARS)) data_replace(content=assert_response, source=GLOBAL_VARS))
# 由于数据库断言里面的变量需要请求响应后进行提取,因此目前不进行处理 # 由于数据库断言里面的变量需要请求响应后进行提取,因此目前不进行处理
except Exception as e: except Exception as e:
logger.error(f"处理assert报错了:{e}") logger.error(f"处理{assert_response}报错了:{e}")
print(f"处理assert报错了:{e}") print(f"处理{assert_response}报错了:{e}")
# ---------------------------------------- 进行请求,请求后的参数提取处理----------------------------------------# # ---------------------------------------- 进行请求,请求后的参数提取处理----------------------------------------#
@@ -272,3 +308,7 @@ def response_type(response: Response) -> str:
return "json" return "json"
except: except:
return "str" return "str"
if __name__ == '__main__':
test = RequestPreDataHandle(request_data={"files": {"file": ["导入TOC订单.xls", "toc.xls"]}})
test.files_handle()
+77 -63
View File
@@ -1,15 +1,18 @@
import requests
from requests import Response
from requests_toolbelt import MultipartEncoder # 第三方模块:pip install requests_toolbelt
from typing import Dict, Union
import time import time
from typing import Dict, Union
import requests
from loguru import logger from loguru import logger
from requests import Response
from requests_toolbelt import MultipartEncoder
class BaseRequest: class BaseRequest:
""" """
进行请求 进行请求
""" """
TIMEOUT = 5
session = None session = None
@classmethod @classmethod
@@ -30,28 +33,28 @@ class BaseRequest:
:return: 响应对象 :return: 响应对象
""" """
try: try:
logger.debug("\n======================================================\n" \ logger.debug("\n" + "=" * 80
"-------------Start:请求前--------------------\n" + "\n-------------Start:请求前--------------------\n"
f"用例标题: {req_data.get('title', None)}\n" \ f"用例标题: {req_data.get('title', None)}\n"
f"请求路径: {req_data.get('url', None)}\n" \ f"请求路径: {req_data.get('url', None)}\n"
f"请求方式: {req_data.get('method', None)}\n" \ f"请求方式: {req_data.get('method', None)}\n"
f"请求头: {req_data.get('headers', None)}\n" \ f"请求头: {req_data.get('headers', None)}\n"
f"请求Cookies: {req_data.get('cookies', None)}\n" \ f"请求Cookies: {req_data.get('cookies', None)}\n"
f"请求关键字: {req_data.get('request_type', None)}\n" \ f"请求关键字: {req_data.get('request_type', None)}\n"
f"请求内容: {req_data.get('payload', None)}\n" \ f"请求内容: {req_data.get('payload', None)}\n"
f"请求文件: {req_data.get('files', None)}\n" \ f"请求文件: {req_data.get('files', None)}\n"
"=====================================================") + "=" * 80)
print("\n======================================================\n" \ print("\n" + "=" * 80
"-------------Start:请求前--------------------\n" + "\n-------------Start:请求前--------------------\n"
f"用例标题: {req_data.get('title', None)}\n" \ f"用例标题: {req_data.get('title', None)}\n"
f"请求路径: {req_data.get('url', None)}\n" \ f"请求路径: {req_data.get('url', None)}\n"
f"请求方式: {req_data.get('method', None)}\n" \ f"请求方式: {req_data.get('method', None)}\n"
f"请求头: {req_data.get('headers', None)}\n" \ f"请求头: {req_data.get('headers', None)}\n"
f"请求Cookies: {req_data.get('cookies', None)}\n" \ f"请求Cookies: {req_data.get('cookies', None)}\n"
f"请求关键字: {req_data.get('request_type', None)}\n" \ f"请求关键字: {req_data.get('request_type', None)}\n"
f"请求内容: {req_data.get('payload', None)}\n" \ f"请求内容: {req_data.get('payload', None)}\n"
f"请求文件: {req_data.get('files', None)}\n" \ f"请求文件: {req_data.get('files', None)}\n"
"=====================================================") + "=" * 80)
res = cls.send_api_request( res = cls.send_api_request(
url=req_data.get("url"), url=req_data.get("url"),
method=req_data.get("method").lower(), method=req_data.get("method").lower(),
@@ -61,16 +64,16 @@ class BaseRequest:
files=req_data.get("files", None), files=req_data.get("files", None),
cookies=req_data.get("cookies", None) cookies=req_data.get("cookies", None)
) )
logger.debug("\n======================================================\n" \ logger.debug("\n" + "=" * 80
"-------------End:请求后--------------------\n" + "\n-------------End:请求后--------------------\n"
f"响应数据: {res.text}\n" \ f"响应数据: {res.text}\n"
f"响应码: {res.status_code}\n" \ f"响应码: {res.status_code}\n"
"=====================================================") + "=" * 80)
print("\n======================================================\n" \ print("\n" + "=" * 80
"-------------End:请求后--------------------\n" + "\n-------------End:请求后--------------------\n"
f"响应数据: {res.text}\n" \ f"响应数据: {res.text}\n"
f"响应码: {res.status_code}\n" \ f"响应码: {res.status_code}\n"
"=====================================================") + "=" * 80)
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
logger.error(f"请求出错,{str(e)}") logger.error(f"请求出错,{str(e)}")
print(f"请求出错,{str(e)}") print(f"请求出错,{str(e)}")
@@ -95,33 +98,44 @@ class BaseRequest:
headers = header or {} headers = header or {}
session = cls.get_session() session = cls.get_session()
if request_type and request_type.lower() == 'params': if request_type:
res = session.request(method=method, url=url, params=payload, headers=headers, cookies=cookies, timeout=5) if request_type.lower() == 'params':
elif request_type and request_type.lower() == 'data': res = session.request(method=method, url=url, params=payload, headers=headers, cookies=cookies,
if files: timeout=cls.TIMEOUT)
if not isinstance(files, dict): return res
raise ValueError('data参数必须为dict') elif request_type.lower() == 'data':
encoder = MultipartEncoder(fields=files, boundary='------------------------' + str(time.time())) res = session.request(method=method, url=url, data=payload, headers=headers, cookies=cookies,
headers['Content-Type'] = encoder.content_type timeout=cls.TIMEOUT)
res = session.request(method=method, url=url, data=encoder.to_string(), headers=headers, return res
cookies=cookies, timeout=5) elif request_type.lower() == 'json':
res = session.request(method=method, url=url, json=payload, headers=headers, cookies=cookies,
timeout=cls.TIMEOUT)
return res
elif request_type.lower() == 'file':
if files:
if payload:
if isinstance(files, dict):
for k, v in payload.items():
files[k] = v
elif isinstance(files, list):
# TODO 这里是应对多文件上传的情况,暂时没有接口帮助验证是否真正上传了多个文件,有可能只上传成功了一个
for k, v in payload.items():
files.append((k, v))
encoder = MultipartEncoder(fields=files, boundary='------------------------' + str(time.time()))
headers['Content-Type'] = encoder.content_type
res = session.request(method=method, url=url, data=encoder.to_string(), headers=headers,
cookies=cookies, timeout=cls.TIMEOUT)
return res
else:
logger.error('上传的文件不能为空')
print('上传的文件不能为空')
else: else:
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8' logger.error('request_type可选关键字为params, json, data, file')
res = session.request(method=method, url=url, data=payload, headers=headers, cookies=cookies, timeout=5) print('request_type可选关键字为params, json, data, file')
elif request_type and request_type.lower() == 'json': raise ValueError('request_type可选关键字为params, json, data, file')
if files:
if not isinstance(files, dict):
raise ValueError('json参数必须为dict')
encoder = MultipartEncoder(fields=files, boundary='------------------------' + str(time.time()))
headers['Content-Type'] = encoder.content_type
res = session.request(method=method, url=url, json=encoder.to_string(), headers=headers,
cookies=cookies, timeout=5)
else:
headers['Content-Type'] = 'application/json'
res = session.request(method=method, url=url, json=payload, headers=headers, cookies=cookies, timeout=5)
else: else:
logger.error('request_type可选关键字为params, json, data') logger.error('request_type参数不能为空')
print('request_type可选关键字为params, json, data') print('request_type参数不能为空')
raise ValueError('request_type可选关键字为params, json, data') raise ValueError('request_type参数不能为空')
return res
+13
View File
@@ -133,3 +133,16 @@ def copy_file(src_file_path, dest_dir_path):
return "复制成功" return "复制成功"
except Exception as e: except Exception as e:
return f"复制失败:{e}" return f"复制失败:{e}"
def get_file_field(file_path):
"""
获取文件名称和二进制内容
:param: file_path: 文件路径
"""
# 处理文件绝对路径
file_name = os.path.basename(file_path)
# 获取文件二进制内容
with open(file_path, 'rb') as f:
file_content = f.read()
return (file_name, file_content)
+4 -1
View File
@@ -17,9 +17,12 @@ COMMON_DIR = os.path.join(BASE_DIR, "common_utils")
# 配置模块目录 # 配置模块目录
CONF_DIR = os.path.join(BASE_DIR, "config") CONF_DIR = os.path.join(BASE_DIR, "config")
# 数据模块目录 # 测试数据模块目录
DATA_DIR = os.path.join(BASE_DIR, "data") DATA_DIR = os.path.join(BASE_DIR, "data")
# 测试文件模块目录
FILES_DIR = os.path.join(BASE_DIR, "files")
# 日志/报告保存目录 # 日志/报告保存目录
OUT_DIR = os.path.join(BASE_DIR, "outputs") OUT_DIR = os.path.join(BASE_DIR, "outputs")
if not os.path.exists(OUT_DIR): if not os.path.exists(OUT_DIR):