使用 pytest 测试框架时,如何将一个接口的请求或响应参数保存下来,以供后续接口请求动态参数化

171 阅读2分钟

在使用 pytest 测试框架时,如果需要将一个接口的请求或响应参数保存下来,以供后续接口请求动态参数化,可以通过以下方法实现:


1. 问题分析

1.1 场景描述

  • 某些接口的参数(如 tokenuser_id)需要从前置接口的响应数据中获取,供后续接口使用。
  • 需要动态替换这些参数,使测试用例支持接口间的依赖。

1.2 解决方案

  1. 全局上下文管理

    • 使用一个全局 context 字典存储动态数据,例如前置接口的返回值。
    • 后续接口通过占位符(如 $response.data.token$global.user_id)引用这些动态数据。
  2. 参数化解析

    • 在运行时解析测试用例中的占位符,将其替换为上下文中的实际值。
  3. 保存动态数据

    • 在接口执行后,将响应中的特定字段提取并保存到上下文中。

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, dictand 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, dictand 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_code200
      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_code200
      body:
        user_id"$global.user_id"
        username"test_user"

3. 运行测试

执行命令运行测试用例:

pytest -v

4. 关键点总结

  1. 上下文管理

    • 使用全局 context 存储动态数据,支持跨接口依赖。
  2. 参数化解析

    • 使用占位符(如 $response.data.token)动态替换参数。
  3. 动态数据保存

    • 在接口执行后,将响应中的关键字段保存到上下文。
  4. 灵活扩展

    • 可以通过 save 配置支持更多动态参数的保存。

这种方式适用于复杂的接口测试场景,尤其是多接口之间存在强依赖关系时,非常高效且易于维护。