在使用 pytest 测试框架时,如果需要将一个接口的请求或响应参数保存下来,以供后续接口请求动态参数化,可以通过以下方法实现:
1. 问题分析
1.1 场景描述
- 某些接口的参数(如
token、user_id)需要从前置接口的响应数据中获取,供后续接口使用。 - 需要动态替换这些参数,使测试用例支持接口间的依赖。
1.2 解决方案
-
全局上下文管理:
- 使用一个全局
context字典存储动态数据,例如前置接口的返回值。 - 后续接口通过占位符(如
$response.data.token或$global.user_id)引用这些动态数据。
- 使用一个全局
-
参数化解析:
- 在运行时解析测试用例中的占位符,将其替换为上下文中的实际值。
-
保存动态数据:
- 在接口执行后,将响应中的特定字段提取并保存到上下文中。
2. 实现方法
将需要保存的请求参数或响应数据存储到一个全局上下文中,以供后续接口使用。
2.1 上下文管理
定义一个全局 context 字典,用于存储和共享接口之间的动态数据。
# 全局上下文
context = {
"request": {}, # 存储请求数据
"response": {}, # 存储响应数据
"global": {}, # 存储全局动态变量
}
2.2 参数解析器
实现一个递归解析器,用于解析占位符(如 $response.data.token)。解析器从上下文中查找对应的值并替换占位符。
import re
def resolve_placeholders(data, context):
"""
递归解析占位符,支持从上下文中获取动态参数
:param data: 待解析的数据(字符串、字典或列表)
:param context: 全局上下文对象
:return: 解析后的数据
"""
if isinstance(data, str):
# 匹配占位符格式,如 $response.data.token 或 $global.user_id
matches = re.findall(r"$([\w.]+)", data)
for match in matches:
keys = match.split('.')
value = get_nested_value(context, keys)
if value is not None:
# 替换占位符为实际值
data = data.replace(f"${{{match}}}", str(value))
return data
elif isinstance(data, dict):
# 递归解析字典
return {k: resolve_placeholders(v, context) for k, v in data.items()}
elif isinstance(data, list):
# 递归解析列表
return [resolve_placeholders(item, context) for item in data]
else:
return data
def get_nested_value(data, keys):
"""
根据键路径获取嵌套值
:param data: 数据字典
:param keys: 键路径列表
:return: 嵌套值
"""
for key in keys:
if isinstance(data, dict) and key in data:
data = data[key]
else:
return None
return data
2.3 保存动态数据
在每次接口执行后,将需要保存的参数存储到上下文中。例如,将登录接口返回的 token 保存到 context["global"]["token"]。
def save_to_context(response, context, save_mapping):
"""
根据映射规则,将响应数据保存到上下文
:param response: 接口响应数据(dict)
:param context: 全局上下文对象
:param save_mapping: 保存规则,指定需要提取的字段
"""
for key, path in save_mapping.items():
value = get_nested_value(response, path.split('.'))
if value is not None:
context["global"][key] = value
示例保存规则:
# 将登录接口返回的 token 和 user_id 保存到上下文
save_mapping = {
"token": "data.token",
"user_id": "data.user_id"
}
save_to_context(response.json(), context, save_mapping)
2.4 测试框架实现
结合之前的框架,整合参数解析器和动态数据保存逻辑。
完整代码示例
import pytest
import requests
import yaml
# 全局上下文
context = {
"request": {},
"response": {},
"global": {}
}
def load_test_cases(file_path):
"""加载测试用例"""
with open(file_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)["test_cases"]
def save_to_context(response, context, save_mapping):
"""保存动态数据到上下文"""
for key, path in save_mapping.items():
value = get_nested_value(response, path.split('.'))
if value is not None:
context["global"][key] = value
def resolve_placeholders(data, context):
"""解析占位符"""
if isinstance(data, str):
matches = re.findall(r"$([\w.]+)", data)
for match in matches:
keys = match.split('.')
value = get_nested_value(context, keys)
if value is not None:
data = data.replace(f"${{{match}}}", str(value))
return data
elif isinstance(data, dict):
return {k: resolve_placeholders(v, context) for k, v in data.items()}
elif isinstance(data, list):
return [resolve_placeholders(item, context) for item in data]
else:
return data
def get_nested_value(data, keys):
"""获取嵌套值"""
for key in keys:
if isinstance(data, dict) and key in data:
data = data[key]
else:
return None
return data
# 加载测试用例
test_cases = load_test_cases("test_cases.yml")
@pytest.mark.parametrize("case", test_cases)
def test_api(case):
# 1. 解析请求参数(占位符替换)
request_data = resolve_placeholders(case["request"], context)
# 2. 发送请求
response = requests.request(
method=request_data["method"],
url=request_data["url"],
headers=request_data.get("headers"),
json=request_data.get("body")
)
# 3. 保存请求和响应数据到上下文
context["request"][case["name"]] = request_data
context["response"][case["name"]] = response.json()
# 如果存在保存规则,将指定字段保存到上下文
save_mapping = case.get("save", {})
save_to_context(response.json(), context, save_mapping)
# 4. 验证响应
expected_data = resolve_placeholders(case["expected"], context)
assert response.status_code == expected_data["status_code"]
for key, value in expected_data.get("body", {}).items():
assert value == get_nested_value(response.json(), key.split('.'))
2.5 测试用例示例
调整 test_cases.yml,支持动态保存和参数化。
test_cases:
- name: "用户登录"
request:
method: POST
url: "http://example.com/api/login"
headers:
Content-Type: "application/json"
body:
username: "test_user"
password: "test_password"
expected:
status_code: 200
body:
token: "$response.data.token"
save:
token: "data.token"
user_id: "data.user_id"
- name: "获取用户信息"
request:
method: GET
url: "http://example.com/api/users/$global.user_id"
headers:
Authorization: "Bearer $global.token"
expected:
status_code: 200
body:
user_id: "$global.user_id"
username: "test_user"
3. 运行测试
执行命令运行测试用例:
pytest -v
4. 关键点总结
-
上下文管理:
- 使用全局
context存储动态数据,支持跨接口依赖。
- 使用全局
-
参数化解析:
- 使用占位符(如
$response.data.token)动态替换参数。
- 使用占位符(如
-
动态数据保存:
- 在接口执行后,将响应中的关键字段保存到上下文。
-
灵活扩展:
- 可以通过
save配置支持更多动态参数的保存。
- 可以通过
这种方式适用于复杂的接口测试场景,尤其是多接口之间存在强依赖关系时,非常高效且易于维护。