19.pytest的jsonpath

Pytest 的 JSONPath 详解

1. 什么是 JSONPath

1.1 JSONPath 的概念

JSONPath 是一种用于从 JSON 文档中提取数据的查询语言,类似于 XPath 用于 XML 文档。它提供了一种简洁、直观的方式来定位和提取 JSON 数据中的特定部分。

1.2 为什么需要 JSONPath

在 API 测试中,我们经常需要从复杂的 JSON 响应中提取数据。传统的 Python 字典访问方式(如 response["data"]["user"]["id"])虽然可行,但存在以下问题:

  1. 代码冗长:嵌套层级深时,代码会变得很长
  2. 容易出错:键名拼写错误会导致 KeyError
  3. 不够灵活:难以处理动态结构或数组数据
  4. 可读性差:多层嵌套的字典访问不够直观

JSONPath 解决了这些问题,提供了更优雅、更强大的数据提取方式。

1.3 JSONPath 的优势

  • 简洁直观:使用类似文件路径的语法,易于理解
  • 功能强大:支持通配符、过滤、切片等高级功能
  • 跨语言:JSONPath 是标准化的,可以在多种编程语言中使用
  • 灵活性强:可以处理复杂的嵌套结构和数组

1.4 JSONPath 与 Python 字典访问的对比

让我们看一个简单的对比示例:

# 假设我们有这样的 JSON 响应
response_data = {
    "code": 200,
    "message": "success",
    "data": {
        "users": [
            {"id": 1, "name": "Alice", "email": "alice@example.com"},
            {"id": 2, "name": "Bob", "email": "bob@example.com"},
            {"id": 3, "name": "Charlie", "email": "charlie@example.com"}
        ],
        "total": 3
    }
}

# 传统方式:提取第一个用户的邮箱
email = response_data["data"]["users"][0]["email"]
print(email)  # 输出: alice@example.com

# JSONPath 方式:提取第一个用户的邮箱
from jsonpath_ng import parse
jsonpath_expr = parse("$.data.users[0].email")
matches = [match.value for match in jsonpath_expr.find(response_data)]
email = matches[0] if matches else None
print(email)  # 输出: alice@example.com

# JSONPath 方式:提取所有用户的邮箱(更强大)
jsonpath_expr = parse("$.data.users[*].email")
matches = [match.value for match in jsonpath_expr.find(response_data)]
print(matches)  # 输出: ['alice@example.com', 'bob@example.com', 'charlie@example.com']

可以看到,JSONPath 在处理数组数据时更加灵活和强大。


2. 安装 JSONPath 库

2.1 可用的 JSONPath 库

Python 中有几个流行的 JSONPath 库:

  1. jsonpath-ng:功能最全面,支持完整的 JSONPath 规范
  2. jsonpath-rw:另一个流行的实现
  3. jsonpath:轻量级的实现

我们推荐使用 jsonpath-ng,因为它功能最完整,文档最详细。

2.2 安装 jsonpath-ng

# 使用 pip 安装
pip install jsonpath-ng

# 使用 pip 从国内镜像源安装(推荐)
pip install jsonpath-ng -i https://pypi.tuna.tsinghua.edu.cn/simple

# 安装指定版本
pip install jsonpath-ng==1.6.0

2.3 验证安装

安装完成后,可以通过以下方式验证:

# 方式一:检查版本
import jsonpath_ng
print(jsonpath_ng.__version__)  # 输出版本号

# 方式二:尝试导入和使用
from jsonpath_ng import parse
print("安装成功!")

# 方式三:简单测试
data = {"name": "test"}
jsonpath_expr = parse("$.name")
matches = [match.value for match in jsonpath_expr.find(data)]
print(matches)  # 输出: ['test']

3. JSONPath 基础语法

3.1 JSONPath 表达式结构

JSONPath 表达式以 $ 开头,表示 JSON 文档的根节点。然后使用点号(.)或方括号([])来访问子节点。

3.2 基本操作符

3.2.1 根节点操作符

  • $:表示 JSON 文档的根节点
from jsonpath_ng import parse

data = {"name": "Alice", "age": 30}

# 访问根节点
jsonpath_expr = parse("$")
matches = [match.value for match in jsonpath_expr.find(data)]
print(matches)  # 输出: [{'name': 'Alice', 'age': 30}]

3.2.2 子节点操作符

  • .:点号用于访问对象的属性
  • []:方括号用于访问数组元素或对象的属性
from jsonpath_ng import parse

data = {
    "user": {
        "name": "Alice",
        "age": 30
    },
    "items": [1, 2, 3]
}

# 使用点号访问属性
jsonpath_expr = parse("$.user.name")
matches = [match.value for match in jsonpath_expr.find(data)]
print(matches)  # 输出: ['Alice']

# 使用方括号访问属性
jsonpath_expr = parse("$['user']['name']")
matches = [match.value for match in jsonpath_expr.find(data)]
print(matches)  # 输出: ['Alice']

# 访问数组元素
jsonpath_expr = parse("$.items[0]")
matches = [match.value for match in jsonpath_expr.find(data)]
print(matches)  # 输出: [1]

3.2.3 通配符操作符

  • *``**:匹配所有元素或属性
from jsonpath_ng import parse

data = {
    "users": [
        {"name": "Alice", "age": 30},
        {"name": "Bob", "age": 25},
        {"name": "Charlie", "age": 35}
    ]
}

# 匹配所有用户
jsonpath_expr = parse("$.users[*]")
matches = [match.value for match in jsonpath_expr.find(data)]
print(matches)
# 输出: [{'name': 'Alice', 'age': 30}, {'name': 'Bob', 'age': 25}, {'name': 'Charlie', 'age': 35}]

# 匹配所有用户的姓名
jsonpath_expr = parse("$.users[*].name")
matches = [match.value for match in jsonpath_expr.find(data)]
print(matches)  # 输出: ['Alice', 'Bob', 'Charlie']

3.2.4 数组切片操作符

  • [start:end:step]:Python 风格的数组切片
from jsonpath_ng import parse

data = {
    "numbers": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}

# 获取前三个元素
jsonpath_expr = parse("$.numbers[0:3]")
matches = [match.value for match in jsonpath_expr.find(data)]
print(matches)  # 输出: [0, 1, 2]

# 获取所有偶数索引的元素
jsonpath_expr = parse("$.numbers[0::2]")
matches = [match.value for match in jsonpath_expr.find(data)]
print(matches)  # 输出: [0, 2, 4, 6, 8]

# 获取最后三个元素
jsonpath_expr = parse("$.numbers[-3:]")
matches = [match.value for match in jsonpath_expr.find(data)]
print(matches)  # 输出: [7, 8, 9]

3.2.5 递归下降操作符

  • ..:递归搜索所有匹配的节点
from jsonpath_ng import parse

data = {
    "store": {
        "book": [
            {"title": "Book 1", "author": "Author 1"},
            {"title": "Book 2", "author": "Author 2"}
        ],
        "bicycle": {
            "color": "red",
            "price": 19.95
        }
    }
}

# 递归查找所有 price 字段
jsonpath_expr = parse("$..price")
matches = [match.value for match in jsonpath_expr.find(data)]
print(matches)  # 输出: [19.95]

# 递归查找所有 title 字段
jsonpath_expr = parse("$..title")
matches = [match.value for match in jsonpath_expr.find(data)]
print(matches)  # 输出: ['Book 1', 'Book 2']

3.3 完整示例:理解 JSONPath 语法

让我们通过一个完整的示例来理解各种 JSONPath 语法:

from jsonpath_ng import parse

# 复杂的 JSON 数据结构
data = {
    "store": {
        "book": [
            {
                "category": "reference",
                "author": "Nigel Rees",
                "title": "Sayings of the Century",
                "price": 8.95
            },
            {
                "category": "fiction",
                "author": "Evelyn Waugh",
                "title": "Sword of Honour",
                "price": 12.99
            },
            {
                "category": "fiction",
                "author": "Herman Melville",
                "title": "Moby Dick",
                "isbn": "0-553-21311-3",
                "price": 8.99
            },
            {
                "category": "fiction",
                "author": "J. R. R. Tolkien",
                "title": "The Lord of the Rings",
                "isbn": "0-395-19395-8",
                "price": 22.99
            }
        ],
        "bicycle": {
            "color": "red",
            "price": 19.95
        }
    }
}

# 1. 获取所有书籍
jsonpath_expr = parse("$.store.book[*]")
matches = [match.value for match in jsonpath_expr.find(data)]
print(f"所有书籍数量: {len(matches)}")  # 输出: 4

# 2. 获取所有书籍的价格
jsonpath_expr = parse("$.store.book[*].price")
matches = [match.value for match in jsonpath_expr.find(data)]
print(f"所有价格: {matches}")  # 输出: [8.95, 12.99, 8.99, 22.99]

# 3. 获取第一本书
jsonpath_expr = parse("$.store.book[0]")
matches = [match.value for match in jsonpath_expr.find(data)]
print(f"第一本书: {matches[0]['title']}")  # 输出: Sayings of the Century

# 4. 获取最后一本书
jsonpath_expr = parse("$.store.book[-1]")
matches = [match.value for match in jsonpath_expr.find(data)]
print(f"最后一本书: {matches[0]['title']}")  # 输出: The Lord of the Rings

# 5. 获取前两本书
jsonpath_expr = parse("$.store.book[0:2]")
matches = [match.value for match in jsonpath_expr.find(data)]
print(f"前两本书数量: {len(matches)}")  # 输出: 2

# 6. 递归查找所有价格
jsonpath_expr = parse("$..price")
matches = [match.value for match in jsonpath_expr.find(data)]
print(f"所有价格(递归): {matches}")  # 输出: [8.95, 12.99, 8.99, 22.99, 19.95]

# 7. 获取所有有 ISBN 的书籍
jsonpath_expr = parse("$.store.book[?(@.isbn)]")
matches = [match.value for match in jsonpath_expr.find(data)]
print(f"有 ISBN 的书籍数量: {len(matches)}")  # 输出: 2

4. 在 Pytest 中使用 JSONPath

4.1 基本使用方式

在 pytest 测试中使用 JSONPath 提取数据非常简单:

import pytest
from jsonpath_ng import parse

def test_extract_simple_value():
    """测试提取简单值"""
    data = {
        "code": 200,
        "message": "success",
        "data": {
            "user_id": 12345,
            "username": "test_user"
        }
    }

    # 提取 user_id
    jsonpath_expr = parse("$.data.user_id")
    matches = [match.value for match in jsonpath_expr.find(data)]
    user_id = matches[0] if matches else None

    assert user_id == 12345
    print(f"提取的用户 ID: {user_id}")

4.2 创建辅助函数

为了更方便地在测试中使用 JSONPath,我们可以创建一个辅助函数:

import pytest
from jsonpath_ng import parse
from typing import Any, List, Optional

def extract_jsonpath(data: dict, jsonpath_str: str, default: Any = None) -> Any:
    """
    从 JSON 数据中提取值

    参数:
        data: JSON 数据(字典)
        jsonpath_str: JSONPath 表达式
        default: 如果未找到时的默认值

    返回:
        提取的值,如果未找到则返回 default
    """
    try:
        jsonpath_expr = parse(jsonpath_str)
        matches = [match.value for match in jsonpath_expr.find(data)]
        if matches:
            # 如果只有一个结果,直接返回;如果有多个,返回列表
            return matches[0] if len(matches) == 1 else matches
        return default
    except Exception as e:
        print(f"JSONPath 提取失败: {e}")
        return default

def extract_jsonpath_list(data: dict, jsonpath_str: str) -> List[Any]:
    """
    从 JSON 数据中提取多个值(总是返回列表)

    参数:
        data: JSON 数据(字典)
        jsonpath_str: JSONPath 表达式

    返回:
        提取的值列表
    """
    try:
        jsonpath_expr = parse(jsonpath_str)
        matches = [match.value for match in jsonpath_expr.find(data)]
        return matches
    except Exception as e:
        print(f"JSONPath 提取失败: {e}")
        return []

# 使用辅助函数
def test_with_helper_function():
    """使用辅助函数提取数据"""
    data = {
        "users": [
            {"id": 1, "name": "Alice"},
            {"id": 2, "name": "Bob"}
        ]
    }

    # 提取单个值
    first_user_name = extract_jsonpath(data, "$.users[0].name")
    assert first_user_name == "Alice"

    # 提取多个值
    all_names = extract_jsonpath_list(data, "$.users[*].name")
    assert all_names == ["Alice", "Bob"]

4.3 在 Fixture 中使用 JSONPath

我们可以创建 fixture 来封装 JSONPath 提取逻辑:

import pytest
from jsonpath_ng import parse
import requests

@pytest.fixture
def api_response():
    """模拟 API 响应"""
    return {
        "code": 200,
        "message": "success",
        "data": {
            "token": "abc123xyz789",
            "user": {
                "id": 12345,
                "username": "test_user",
                "email": "test@example.com"
            },
            "expires_in": 3600
        }
    }

@pytest.fixture
def auth_token(api_response):
    """从 API 响应中提取 token"""
    jsonpath_expr = parse("$.data.token")
    matches = [match.value for match in jsonpath_expr.find(api_response)]
    return matches[0] if matches else None

@pytest.fixture
def user_id(api_response):
    """从 API 响应中提取用户 ID"""
    jsonpath_expr = parse("$.data.user.id")
    matches = [match.value for match in jsonpath_expr.find(api_response)]
    return matches[0] if matches else None

@pytest.fixture
def user_email(api_response):
    """从 API 响应中提取用户邮箱"""
    jsonpath_expr = parse("$.data.user.email")
    matches = [match.value for match in jsonpath_expr.find(api_response)]
    return matches[0] if matches else None

def test_use_extracted_values(auth_token, user_id, user_email):
    """使用提取的值进行测试"""
    assert auth_token == "abc123xyz789"
    assert user_id == 12345
    assert user_email == "test@example.com"

    print(f"Token: {auth_token}")
    print(f"User ID: {user_id}")
    print(f"Email: {user_email}")

4.4 从真实 API 响应中提取数据

在实际的 API 测试中,我们可以这样使用 JSONPath:

import pytest
from jsonpath_ng import parse
import requests

@pytest.fixture
def login_response():
    """登录接口响应"""
    url = "https://api.example.com/login"
    data = {
        "username": "test_user",
        "password": "123456"
    }
    response = requests.post(url, json=data)
    return response.json()

@pytest.fixture
def auth_token(login_response):
    """从登录响应中提取 token"""
    jsonpath_expr = parse("$.data.token")
    matches = [match.value for match in jsonpath_expr.find(login_response)]
    token = matches[0] if matches else None

    assert token is not None, "Token 提取失败"
    return token

@pytest.fixture
def user_info(login_response):
    """从登录响应中提取用户信息"""
    jsonpath_expr = parse("$.data.user")
    matches = [match.value for match in jsonpath_expr.find(login_response)]
    return matches[0] if matches else {}

def test_get_user_profile(auth_token, user_info):
    """使用提取的 token 获取用户资料"""
    # 从 user_info 中提取用户 ID
    user_id = user_info.get("id")

    # 使用 token 发送请求
    url = f"https://api.example.com/users/{user_id}"
    headers = {"Authorization": f"Bearer {auth_token}"}
    response = requests.get(url, headers=headers)

    assert response.status_code == 200
    profile_data = response.json()

    # 使用 JSONPath 验证响应数据
    jsonpath_expr = parse("$.data.username")
    matches = [match.value for match in jsonpath_expr.find(profile_data)]
    username = matches[0] if matches else None

    assert username == "test_user"

5. JSONPath 高级用法

5.1 过滤表达式

JSONPath 支持使用过滤表达式来筛选数据:

from jsonpath_ng import parse

data = {
    "users": [
        {"id": 1, "name": "Alice", "age": 30, "active": True},
        {"id": 2, "name": "Bob", "age": 25, "active": False},
        {"id": 3, "name": "Charlie", "age": 35, "active": True},
        {"id": 4, "name": "David", "age": 28, "active": True}
    ]
}

# 注意:jsonpath-ng 的过滤语法可能与其他实现不同
# 这里展示基本用法,实际使用时请参考 jsonpath-ng 文档

# 提取所有活跃用户的姓名
# 注意:jsonpath-ng 的过滤语法需要使用特定的方式
# 这里我们先用 Python 代码演示概念

# 提取所有年龄大于 28 的用户
# 在实际项目中,可能需要结合 Python 代码来实现复杂过滤

5.2 处理数组数据

JSONPath 在处理数组数据时非常强大:

from jsonpath_ng import parse

data = {
    "orders": [
        {
            "id": 1001,
            "items": [
                {"product_id": 1, "quantity": 2, "price": 10.0},
                {"product_id": 2, "quantity": 1, "price": 20.0}
            ],
            "total": 40.0
        },
        {
            "id": 1002,
            "items": [
                {"product_id": 3, "quantity": 3, "price": 15.0}
            ],
            "total": 45.0
        }
    ]
}

# 提取所有订单 ID
jsonpath_expr = parse("$.orders[*].id")
matches = [match.value for match in jsonpath_expr.find(data)]
print(f"所有订单 ID: {matches}")  # 输出: [1001, 1002]

# 提取所有订单的总金额
jsonpath_expr = parse("$.orders[*].total")
matches = [match.value for match in jsonpath_expr.find(data)]
print(f"所有订单总金额: {matches}")  # 输出: [40.0, 45.0]

# 提取第一个订单的所有商品 ID
jsonpath_expr = parse("$.orders[0].items[*].product_id")
matches = [match.value for match in jsonpath_expr.find(data)]
print(f"第一个订单的商品 ID: {matches}")  # 输出: [1, 2]

# 提取所有订单的所有商品 ID(嵌套数组)
jsonpath_expr = parse("$.orders[*].items[*].product_id")
matches = [match.value for match in jsonpath_expr.find(data)]
print(f"所有商品 ID: {matches}")  # 输出: [1, 2, 3]

5.3 处理嵌套结构

JSONPath 可以轻松处理深层嵌套的 JSON 结构:

from jsonpath_ng import parse

data = {
    "response": {
        "status": {
            "code": 200,
            "message": "OK"
        },
        "payload": {
            "user": {
                "profile": {
                    "personal": {
                        "name": "Alice",
                        "age": 30
                    },
                    "contact": {
                        "email": "alice@example.com",
                        "phone": "123-456-7890"
                    }
                }
            }
        }
    }
}

# 传统方式:需要多层嵌套访问
name = data["response"]["payload"]["user"]["profile"]["personal"]["name"]
print(f"传统方式: {name}")  # 输出: Alice

# JSONPath 方式:简洁明了
jsonpath_expr = parse("$.response.payload.user.profile.personal.name")
matches = [match.value for match in jsonpath_expr.find(data)]
name = matches[0] if matches else None
print(f"JSONPath 方式: {name}")  # 输出: Alice

# 使用递归下降操作符查找所有 email 字段
jsonpath_expr = parse("$..email")
matches = [match.value for match in jsonpath_expr.find(data)]
print(f"递归查找 email: {matches}")  # 输出: ['alice@example.com']

5.4 处理可选字段

在实际 API 测试中,某些字段可能不存在。JSONPath 可以帮助我们安全地提取这些字段:

from jsonpath_ng import parse

def safe_extract(data: dict, jsonpath_str: str, default: Any = None) -> Any:
    """安全地提取 JSONPath 值,处理字段不存在的情况"""
    try:
        jsonpath_expr = parse(jsonpath_str)
        matches = [match.value for match in jsonpath_expr.find(data)]
        return matches[0] if matches else default
    except Exception:
        return default

# 测试数据:有些用户有 email,有些没有
data = {
    "users": [
        {"id": 1, "name": "Alice", "email": "alice@example.com"},
        {"id": 2, "name": "Bob"},  # 没有 email 字段
        {"id": 3, "name": "Charlie", "email": "charlie@example.com"}
    ]
}

# 安全地提取所有用户的 email(不存在的返回 None)
for i in range(len(data["users"])):
    email = safe_extract(data, f"$.users[{i}].email", default="无邮箱")
    print(f"用户 {i+1} 的邮箱: {email}")

# 输出:
# 用户 1 的邮箱: alice@example.com
# 用户 2 的邮箱: 无邮箱
# 用户 3 的邮箱: charlie@example.com

6. 实际应用场景

6.1 场景一:API 测试中的 Token 提取

import pytest
from jsonpath_ng import parse
import requests

class TestAPIAuthentication:
    """API 认证测试"""

    @pytest.fixture
    def login_response(self):
        """登录接口响应"""
        url = "https://api.example.com/login"
        data = {"username": "test_user", "password": "123456"}
        response = requests.post(url, json=data)
        return response.json()

    @pytest.fixture
    def auth_token(self, login_response):
        """从登录响应中提取 token"""
        jsonpath_expr = parse("$.data.token")
        matches = [match.value for match in jsonpath_expr.find(login_response)]
        token = matches[0] if matches else None

        assert token is not None, "Token 提取失败"
        assert len(token) > 0, "Token 不能为空"
        return token

    @pytest.fixture
    def refresh_token(self, login_response):
        """从登录响应中提取刷新 token"""
        jsonpath_expr = parse("$.data.refresh_token")
        matches = [match.value for match in jsonpath_expr.find(login_response)]
        return matches[0] if matches else None

    @pytest.fixture
    def token_expires_in(self, login_response):
        """从登录响应中提取 token 过期时间"""
        jsonpath_expr = parse("$.data.expires_in")
        matches = [match.value for match in jsonpath_expr.find(login_response)]
        return matches[0] if matches else None

    def test_token_extraction(self, auth_token, refresh_token, token_expires_in):
        """测试 token 提取"""
        print(f"Auth Token: {auth_token}")
        print(f"Refresh Token: {refresh_token}")
        print(f"Expires In: {token_expires_in} 秒")

        assert auth_token is not None
        if refresh_token:
            assert refresh_token is not None
        if token_expires_in:
            assert token_expires_in > 0

    def test_use_token_for_api_call(self, auth_token):
        """使用提取的 token 调用受保护的 API"""
        url = "https://api.example.com/protected"
        headers = {"Authorization": f"Bearer {auth_token}"}
        response = requests.get(url, headers=headers)

        assert response.status_code == 200

        # 使用 JSONPath 验证响应
        response_data = response.json()
        jsonpath_expr = parse("$.data.user_id")
        matches = [match.value for match in jsonpath_expr.find(response_data)]
        user_id = matches[0] if matches else None

        assert user_id is not None
        print(f"用户 ID: {user_id}")

6.2 场景二:提取列表数据并验证

import pytest
from jsonpath_ng import parse
import requests

class TestUserList:
    """用户列表测试"""

    @pytest.fixture
    def users_response(self):
        """获取用户列表响应"""
        url = "https://api.example.com/users"
        response = requests.get(url)
        return response.json()

    @pytest.fixture
    def user_list(self, users_response):
        """提取用户列表"""
        jsonpath_expr = parse("$.data.users[*]")
        matches = [match.value for match in jsonpath_expr.find(users_response)]
        return matches

    @pytest.fixture
    def user_ids(self, users_response):
        """提取所有用户 ID"""
        jsonpath_expr = parse("$.data.users[*].id")
        matches = [match.value for match in jsonpath_expr.find(users_response)]
        return matches

    @pytest.fixture
    def user_names(self, users_response):
        """提取所有用户名"""
        jsonpath_expr = parse("$.data.users[*].name")
        matches = [match.value for match in jsonpath_expr.find(users_response)]
        return matches

    @pytest.fixture
    def total_count(self, users_response):
        """提取总数"""
        jsonpath_expr = parse("$.data.total")
        matches = [match.value for match in jsonpath_expr.find(users_response)]
        return matches[0] if matches else 0

    def test_user_list_structure(self, user_list, total_count):
        """验证用户列表结构"""
        assert isinstance(user_list, list)
        assert len(user_list) == total_count

        # 验证每个用户都有必需的字段
        for user in user_list:
            assert "id" in user
            assert "name" in user
            assert "email" in user

    def test_user_ids_unique(self, user_ids):
        """验证用户 ID 唯一性"""
        assert len(user_ids) == len(set(user_ids)), "用户 ID 不唯一"
        print(f"用户 ID 列表: {user_ids}")

    def test_user_names_not_empty(self, user_names):
        """验证用户名不为空"""
        for name in user_names:
            assert name is not None
            assert len(name.strip()) > 0
        print(f"用户名列表: {user_names}")

    def test_first_user_details(self, users_response):
        """验证第一个用户的详细信息"""
        # 提取第一个用户的所有信息
        jsonpath_expr = parse("$.data.users[0]")
        matches = [match.value for match in jsonpath_expr.find(users_response)]
        first_user = matches[0] if matches else {}

        assert first_user.get("id") is not None
        assert first_user.get("name") is not None
        assert first_user.get("email") is not None

        print(f"第一个用户: {first_user}")

6.3 场景三:订单流程测试

import pytest
from jsonpath_ng import parse
import requests

class TestOrderFlow:
    """订单流程测试"""

    @pytest.fixture
    def create_order_response(self, auth_token):
        """创建订单响应"""
        url = "https://api.example.com/orders"
        headers = {"Authorization": f"Bearer {auth_token}"}
        data = {
            "product_id": 1001,
            "quantity": 2,
            "address": "123 Main St"
        }
        response = requests.post(url, json=data, headers=headers)
        return response.json()

    @pytest.fixture
    def order_id(self, create_order_response):
        """提取订单 ID"""
        jsonpath_expr = parse("$.data.order_id")
        matches = [match.value for match in jsonpath_expr.find(create_order_response)]
        return matches[0] if matches else None

    @pytest.fixture
    def order_total(self, create_order_response):
        """提取订单总金额"""
        jsonpath_expr = parse("$.data.total")
        matches = [match.value for match in jsonpath_expr.find(create_order_response)]
        return matches[0] if matches else None

    @pytest.fixture
    def order_items(self, create_order_response):
        """提取订单商品列表"""
        jsonpath_expr = parse("$.data.items[*]")
        matches = [match.value for match in jsonpath_expr.find(create_order_response)]
        return matches

    @pytest.fixture
    def order_status(self, create_order_response):
        """提取订单状态"""
        jsonpath_expr = parse("$.data.status")
        matches = [match.value for match in jsonpath_expr.find(create_order_response)]
        return matches[0] if matches else None

    def test_order_creation(self, order_id, order_total, order_status):
        """验证订单创建"""
        assert order_id is not None
        assert order_total > 0
        assert order_status == "pending"

        print(f"订单 ID: {order_id}")
        print(f"订单总金额: {order_total}")
        print(f"订单状态: {order_status}")

    def test_order_items(self, order_items):
        """验证订单商品"""
        assert len(order_items) > 0

        # 提取所有商品 ID
        product_ids = [item.get("product_id") for item in order_items]
        print(f"商品 ID 列表: {product_ids}")

        # 提取所有商品数量
        quantities = [item.get("quantity") for item in order_items]
        print(f"商品数量列表: {quantities}")

        # 验证每个商品都有必需字段
        for item in order_items:
            assert "product_id" in item
            assert "quantity" in item
            assert "price" in item

    def test_get_order_details(self, order_id, auth_token):
        """获取订单详情并验证"""
        url = f"https://api.example.com/orders/{order_id}"
        headers = {"Authorization": f"Bearer {auth_token}"}
        response = requests.get(url, headers=headers)

        assert response.status_code == 200
        order_data = response.json()

        # 使用 JSONPath 提取订单详情
        jsonpath_expr = parse("$.data.order_id")
        matches = [match.value for match in jsonpath_expr.find(order_data)]
        retrieved_order_id = matches[0] if matches else None

        assert retrieved_order_id == order_id

        # 提取订单创建时间
        jsonpath_expr = parse("$.data.created_at")
        matches = [match.value for match in jsonpath_expr.find(order_data)]
        created_at = matches[0] if matches else None

        assert created_at is not None
        print(f"订单创建时间: {created_at}")

6.4 场景四:数据验证和断言

import pytest
from jsonpath_ng import parse

class TestDataValidation:
    """数据验证测试"""

    @pytest.fixture
    def api_response(self):
        """模拟 API 响应"""
        return {
            "code": 200,
            "message": "success",
            "data": {
                "users": [
                    {
                        "id": 1,
                        "name": "Alice",
                        "age": 30,
                        "email": "alice@example.com",
                        "active": True
                    },
                    {
                        "id": 2,
                        "name": "Bob",
                        "age": 25,
                        "email": "bob@example.com",
                        "active": False
                    }
                ],
                "total": 2,
                "page": 1,
                "page_size": 10
            }
        }

    def test_validate_response_structure(self, api_response):
        """验证响应结构"""
        # 验证 code 字段
        jsonpath_expr = parse("$.code")
        matches = [match.value for match in jsonpath_expr.find(api_response)]
        code = matches[0] if matches else None
        assert code == 200

        # 验证 message 字段
        jsonpath_expr = parse("$.message")
        matches = [match.value for match in jsonpath_expr.find(api_response)]
        message = matches[0] if matches else None
        assert message == "success"

        # 验证 data 字段存在
        jsonpath_expr = parse("$.data")
        matches = [match.value for match in jsonpath_expr.find(api_response)]
        assert len(matches) > 0

    def test_validate_user_data(self, api_response):
        """验证用户数据"""
        # 提取所有用户
        jsonpath_expr = parse("$.data.users[*]")
        matches = [match.value for match in jsonpath_expr.find(api_response)]
        users = matches

        assert len(users) == 2

        # 验证每个用户都有必需的字段
        required_fields = ["id", "name", "age", "email", "active"]
        for user in users:
            for field in required_fields:
                assert field in user, f"用户缺少字段: {field}"

    def test_validate_user_ids(self, api_response):
        """验证用户 ID"""
        # 提取所有用户 ID
        jsonpath_expr = parse("$.data.users[*].id")
        matches = [match.value for match in jsonpath_expr.find(api_response)]
        user_ids = matches

        # 验证 ID 都是正整数
        for user_id in user_ids:
            assert isinstance(user_id, int)
            assert user_id > 0

        # 验证 ID 唯一
        assert len(user_ids) == len(set(user_ids))

    def test_validate_user_emails(self, api_response):
        """验证用户邮箱格式"""
        import re

        # 提取所有邮箱
        jsonpath_expr = parse("$.data.users[*].email")
        matches = [match.value for match in jsonpath_expr.find(api_response)]
        emails = matches

        # 验证邮箱格式
        email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$'
        for email in emails:
            assert re.match(email_pattern, email), f"邮箱格式不正确: {email}"

    def test_validate_pagination(self, api_response):
        """验证分页信息"""
        # 提取分页字段
        total = extract_jsonpath(api_response, "$.data.total")
        page = extract_jsonpath(api_response, "$.data.page")
        page_size = extract_jsonpath(api_response, "$.data.page_size")

        assert total >= 0
        assert page > 0
        assert page_size > 0

        # 验证用户数量与 total 一致
        jsonpath_expr = parse("$.data.users[*]")
        matches = [match.value for match in jsonpath_expr.find(api_response)]
        user_count = len(matches)

        assert user_count == total

7. 创建 JSONPath 工具类

为了更方便地在项目中使用 JSONPath,我们可以创建一个工具类:

# jsonpath_utils.py
from jsonpath_ng import parse
from typing import Any, List, Optional, Dict
import json

class JSONPathExtractor:
    """JSONPath 提取工具类"""

    @staticmethod
    def extract(data: Dict, jsonpath_str: str, default: Any = None) -> Any:
        """
        从 JSON 数据中提取单个值

        参数:
            data: JSON 数据(字典)
            jsonpath_str: JSONPath 表达式
            default: 如果未找到时的默认值

        返回:
            提取的值,如果未找到则返回 default
        """
        try:
            jsonpath_expr = parse(jsonpath_str)
            matches = [match.value for match in jsonpath_expr.find(data)]
            if matches:
                return matches[0] if len(matches) == 1 else matches
            return default
        except Exception as e:
            print(f"JSONPath 提取失败: {e}")
            return default

    @staticmethod
    def extract_all(data: Dict, jsonpath_str: str) -> List[Any]:
        """
        从 JSON 数据中提取所有匹配的值(总是返回列表)

        参数:
            data: JSON 数据(字典)
            jsonpath_str: JSONPath 表达式

        返回:
            提取的值列表
        """
        try:
            jsonpath_expr = parse(jsonpath_str)
            matches = [match.value for match in jsonpath_expr.find(data)]
            return matches
        except Exception as e:
            print(f"JSONPath 提取失败: {e}")
            return []

    @staticmethod
    def extract_first(data: Dict, jsonpath_str: str, default: Any = None) -> Any:
        """
        从 JSON 数据中提取第一个匹配的值

        参数:
            data: JSON 数据(字典)
            jsonpath_str: JSONPath 表达式
            default: 如果未找到时的默认值

        返回:
            第一个匹配的值,如果未找到则返回 default
        """
        matches = JSONPathExtractor.extract_all(data, jsonpath_str)
        return matches[0] if matches else default

    @staticmethod
    def extract_last(data: Dict, jsonpath_str: str, default: Any = None) -> Any:
        """
        从 JSON 数据中提取最后一个匹配的值

        参数:
            data: JSON 数据(字典)
            jsonpath_str: JSONPath 表达式
            default: 如果未找到时的默认值

        返回:
            最后一个匹配的值,如果未找到则返回 default
        """
        matches = JSONPathExtractor.extract_all(data, jsonpath_str)
        return matches[-1] if matches else default

    @staticmethod
    def exists(data: Dict, jsonpath_str: str) -> bool:
        """
        检查 JSONPath 表达式是否匹配到数据

        参数:
            data: JSON 数据(字典)
            jsonpath_str: JSONPath 表达式

        返回:
            如果匹配到数据返回 True,否则返回 False
        """
        matches = JSONPathExtractor.extract_all(data, jsonpath_str)
        return len(matches) > 0

    @staticmethod
    def count(data: Dict, jsonpath_str: str) -> int:
        """
        统计 JSONPath 表达式匹配到的数据数量

        参数:
            data: JSON 数据(字典)
            jsonpath_str: JSONPath 表达式

        返回:
            匹配到的数据数量
        """
        matches = JSONPathExtractor.extract_all(data, jsonpath_str)
        return len(matches)

# 使用示例
if __name__ == "__main__":
    data = {
        "users": [
            {"id": 1, "name": "Alice"},
            {"id": 2, "name": "Bob"}
        ]
    }

    # 提取单个值
    first_name = JSONPathExtractor.extract(data, "$.users[0].name")
    print(f"第一个用户名: {first_name}")

    # 提取所有值
    all_names = JSONPathExtractor.extract_all(data, "$.users[*].name")
    print(f"所有用户名: {all_names}")

    # 检查是否存在
    exists = JSONPathExtractor.exists(data, "$.users[*].name")
    print(f"是否存在用户名: {exists}")

    # 统计数量
    count = JSONPathExtractor.count(data, "$.users[*]")
    print(f"用户数量: {count}")

在 pytest 中使用工具类:

import pytest
from jsonpath_utils import JSONPathExtractor

class TestWithJSONPathUtils:
    """使用 JSONPath 工具类的测试"""

    @pytest.fixture
    def api_response(self):
        return {
            "code": 200,
            "data": {
                "users": [
                    {"id": 1, "name": "Alice"},
                    {"id": 2, "name": "Bob"}
                ]
            }
        }

    def test_extract_with_utils(self, api_response):
        """使用工具类提取数据"""
        # 提取单个值
        first_name = JSONPathExtractor.extract(api_response, "$.data.users[0].name")
        assert first_name == "Alice"

        # 提取所有值
        all_names = JSONPathExtractor.extract_all(api_response, "$.data.users[*].name")
        assert all_names == ["Alice", "Bob"]

        # 检查是否存在
        exists = JSONPathExtractor.exists(api_response, "$.data.users[*].name")
        assert exists is True

        # 统计数量
        count = JSONPathExtractor.count(api_response, "$.data.users[*]")
        assert count == 2

8. 在 conftest.py 中配置 JSONPath

我们可以在 conftest.py 中配置 JSONPath 相关的 fixture,让所有测试文件都能使用:

# conftest.py
import pytest
from jsonpath_ng import parse
from typing import Any, Dict

@pytest.fixture
def jsonpath_extractor():
    """JSONPath 提取器 fixture"""
    class Extractor:
        @staticmethod
        def extract(data: Dict, jsonpath_str: str, default: Any = None) -> Any:
            try:
                jsonpath_expr = parse(jsonpath_str)
                matches = [match.value for match in jsonpath_expr.find(data)]
                if matches:
                    return matches[0] if len(matches) == 1 else matches
                return default
            except Exception:
                return default

        @staticmethod
        def extract_all(data: Dict, jsonpath_str: str) -> list:
            try:
                jsonpath_expr = parse(jsonpath_str)
                matches = [match.value for match in jsonpath_expr.find(data)]
                return matches
            except Exception:
                return []

    return Extractor

# 使用示例
def test_with_jsonpath_fixture(jsonpath_extractor):
    """使用 JSONPath fixture"""
    data = {
        "users": [
            {"id": 1, "name": "Alice"},
            {"id": 2, "name": "Bob"}
        ]
    }

    # 使用 fixture 提取数据
    first_name = jsonpath_extractor.extract(data, "$.users[0].name")
    assert first_name == "Alice"

    all_names = jsonpath_extractor.extract_all(data, "$.users[*].name")
    assert all_names == ["Alice", "Bob"]

9. 常见问题和解决方案

9.1 问题一:JSONPath 表达式语法错误

问题描述:JSONPath 表达式写错了,导致提取失败。

解决方案:仔细检查 JSONPath 语法,使用正确的操作符。

from jsonpath_ng import parse

data = {"user": {"name": "Alice"}}

# ❌ 错误:缺少 $ 符号
# jsonpath_expr = parse("user.name")  # 这会报错

# ✅ 正确:以 $ 开头
jsonpath_expr = parse("$.user.name")
matches = [match.value for match in jsonpath_expr.find(data)]
print(matches)  # 输出: ['Alice']

9.2 问题二:字段不存在导致 KeyError

问题描述:JSON 数据中某些字段可能不存在,直接访问会报错。

解决方案:使用安全提取函数,提供默认值。

def safe_extract(data: dict, jsonpath_str: str, default: Any = None) -> Any:
    """安全提取,处理字段不存在的情况"""
    try:
        jsonpath_expr = parse(jsonpath_str)
        matches = [match.value for match in jsonpath_expr.find(data)]
        return matches[0] if matches else default
    except Exception:
        return default

data = {"user": {"name": "Alice"}}  # 没有 email 字段

# 安全提取,字段不存在时返回默认值
email = safe_extract(data, "$.user.email", default="无邮箱")
print(email)  # 输出: 无邮箱

9.3 问题三:提取数组数据时返回单个值还是列表

问题描述:不确定 JSONPath 会返回单个值还是列表。

解决方案:明确使用 extract_allextract_first 方法。

from jsonpath_ng import parse

data = {
    "users": [
        {"name": "Alice"},
        {"name": "Bob"}
    ]
}

# 提取单个元素 - 返回单个值
jsonpath_expr = parse("$.users[0].name")
matches = [match.value for match in jsonpath_expr.find(data)]
first_name = matches[0] if matches else None
print(f"第一个用户名: {first_name}")  # 输出: Alice

# 提取所有元素 - 返回列表
jsonpath_expr = parse("$.users[*].name")
matches = [match.value for match in jsonpath_expr.find(data)]
all_names = matches
print(f"所有用户名: {all_names}")  # 输出: ['Alice', 'Bob']

9.4 问题四:处理嵌套数组数据

问题描述:需要从嵌套的数组中提取数据。

解决方案:使用递归下降操作符或正确的路径。

from jsonpath_ng import parse

data = {
    "orders": [
        {
            "items": [
                {"product_id": 1, "name": "Product 1"},
                {"product_id": 2, "name": "Product 2"}
            ]
        },
        {
            "items": [
                {"product_id": 3, "name": "Product 3"}
            ]
        }
    ]
}

# 提取所有订单的所有商品名称
jsonpath_expr = parse("$.orders[*].items[*].name")
matches = [match.value for match in jsonpath_expr.find(data)]
print(matches)  # 输出: ['Product 1', 'Product 2', 'Product 3']

# 使用递归下降操作符
jsonpath_expr = parse("$..name")
matches = [match.value for match in jsonpath_expr.find(data)]
print(matches)  # 输出: ['Product 1', 'Product 2', 'Product 3']

9.5 问题五:性能问题

问题描述:在大量数据中使用 JSONPath 可能影响性能。

解决方案:缓存编译后的 JSONPath 表达式,避免重复编译。

from jsonpath_ng import parse
from functools import lru_cache

# 缓存编译后的 JSONPath 表达式
@lru_cache(maxsize=100)
def get_compiled_jsonpath(jsonpath_str: str):
    """获取编译后的 JSONPath 表达式(带缓存)"""
    return parse(jsonpath_str)

def extract_with_cache(data: dict, jsonpath_str: str) -> list:
    """使用缓存的 JSONPath 表达式提取数据"""
    jsonpath_expr = get_compiled_jsonpath(jsonpath_str)
    matches = [match.value for match in jsonpath_expr.find(data)]
    return matches

# 使用示例
data = {"users": [{"name": "Alice"}, {"name": "Bob"}]}

# 第一次调用会编译表达式
result1 = extract_with_cache(data, "$.users[*].name")

# 第二次调用使用缓存的表达式(更快)
result2 = extract_with_cache(data, "$.users[*].name")

10. 最佳实践

10.1 使用有意义的变量名

# ❌ 不好的做法
jsonpath_expr = parse("$.data.users[0].id")
matches = [match.value for match in jsonpath_expr.find(data)]
id = matches[0]

# ✅ 好的做法
user_id_jsonpath = "$.data.users[0].id"
jsonpath_expr = parse(user_id_jsonpath)
matches = [match.value for match in jsonpath_expr.find(data)]
user_id = matches[0] if matches else None

10.2 创建辅助函数

# ✅ 创建可重用的辅助函数
def extract_user_id(response_data: dict) -> Optional[int]:
    """从响应中提取用户 ID"""
    jsonpath_expr = parse("$.data.user.id")
    matches = [match.value for match in jsonpath_expr.find(response_data)]
    return matches[0] if matches else None

# 在测试中使用
def test_user_id(api_response):
    user_id = extract_user_id(api_response)
    assert user_id is not None

10.3 添加错误处理

def safe_extract_jsonpath(data: dict, jsonpath_str: str, default: Any = None) -> Any:
    """安全提取 JSONPath,包含完整的错误处理"""
    try:
        jsonpath_expr = parse(jsonpath_str)
        matches = [match.value for match in jsonpath_expr.find(data)]
        if matches:
            return matches[0] if len(matches) == 1 else matches
        return default
    except Exception as e:
        print(f"JSONPath 提取失败: {jsonpath_str}, 错误: {e}")
        return default

10.4 使用类型提示

from typing import Optional, List, Dict, Any

def extract_jsonpath(
    data: Dict[str, Any], 
    jsonpath_str: str, 
    default: Any = None
) -> Optional[Any]:
    """提取 JSONPath 值,带类型提示"""
    try:
        jsonpath_expr = parse(jsonpath_str)
        matches = [match.value for match in jsonpath_expr.find(data)]
        return matches[0] if matches else default
    except Exception:
        return default

10.5 文档化 JSONPath 表达式

# ✅ 在代码中注释说明 JSONPath 表达式的含义
def extract_auth_token(login_response: dict) -> Optional[str]:
    """
    从登录响应中提取认证 token

    参数:
        login_response: 登录接口的响应数据

    返回:
        认证 token,如果不存在则返回 None

    JSONPath 表达式说明:
        $.data.token - 从响应数据的 data 字段中提取 token
    """
    jsonpath_str = "$.data.token"  # 提取路径:response -> data -> token
    jsonpath_expr = parse(jsonpath_str)
    matches = [match.value for match in jsonpath_expr.find(login_response)]
    return matches[0] if matches else None

11. 完整示例:API 测试项目

让我们创建一个完整的 API 测试项目示例,展示如何在实际项目中使用 JSONPath:

# test_api_with_jsonpath.py
import pytest
from jsonpath_ng import parse
from typing import Optional, List, Dict, Any
import requests

class JSONPathHelper:
    """JSONPath 辅助类"""

    @staticmethod
    def extract(data: Dict, jsonpath_str: str, default: Any = None) -> Any:
        """提取单个值"""
        try:
            jsonpath_expr = parse(jsonpath_str)
            matches = [match.value for match in jsonpath_expr.find(data)]
            return matches[0] if matches else default
        except Exception:
            return default

    @staticmethod
    def extract_all(data: Dict, jsonpath_str: str) -> List[Any]:
        """提取所有匹配的值"""
        try:
            jsonpath_expr = parse(jsonpath_str)
            matches = [match.value for match in jsonpath_expr.find(data)]
            return matches
        except Exception:
            return []

class TestUserAPI:
    """用户 API 测试"""

    BASE_URL = "https://api.example.com"

    @pytest.fixture
    def login_response(self):
        """登录并获取响应"""
        url = f"{self.BASE_URL}/login"
        data = {"username": "test_user", "password": "123456"}
        response = requests.post(url, json=data)
        return response.json()

    @pytest.fixture
    def auth_token(self, login_response):
        """从登录响应中提取 token"""
        token = JSONPathHelper.extract(login_response, "$.data.token")
        assert token is not None, "Token 提取失败"
        return token

    @pytest.fixture
    def user_id(self, login_response):
        """从登录响应中提取用户 ID"""
        user_id = JSONPathHelper.extract(login_response, "$.data.user.id")
        assert user_id is not None, "用户 ID 提取失败"
        return user_id

    def test_login_success(self, login_response):
        """测试登录成功"""
        # 验证响应码
        code = JSONPathHelper.extract(login_response, "$.code")
        assert code == 200

        # 验证消息
        message = JSONPathHelper.extract(login_response, "$.message")
        assert message == "success"

        # 验证 token 存在
        token = JSONPathHelper.extract(login_response, "$.data.token")
        assert token is not None
        assert len(token) > 0

    def test_get_user_list(self, auth_token):
        """测试获取用户列表"""
        url = f"{self.BASE_URL}/users"
        headers = {"Authorization": f"Bearer {auth_token}"}
        response = requests.get(url, headers=headers)

        assert response.status_code == 200
        response_data = response.json()

        # 提取用户列表
        users = JSONPathHelper.extract_all(response_data, "$.data.users[*]")
        assert len(users) > 0

        # 提取所有用户 ID
        user_ids = JSONPathHelper.extract_all(response_data, "$.data.users[*].id")
        assert len(user_ids) == len(users)

        # 验证用户 ID 唯一
        assert len(user_ids) == len(set(user_ids))

        # 提取所有用户名
        user_names = JSONPathHelper.extract_all(response_data, "$.data.users[*].name")
        assert len(user_names) == len(users)

        # 验证每个用户名都不为空
        for name in user_names:
            assert name is not None
            assert len(name.strip()) > 0

    def test_get_user_detail(self, auth_token, user_id):
        """测试获取用户详情"""
        url = f"{self.BASE_URL}/users/{user_id}"
        headers = {"Authorization": f"Bearer {auth_token}"}
        response = requests.get(url, headers=headers)

        assert response.status_code == 200
        response_data = response.json()

        # 验证用户 ID 匹配
        retrieved_user_id = JSONPathHelper.extract(response_data, "$.data.id")
        assert retrieved_user_id == user_id

        # 验证用户信息完整性
        username = JSONPathHelper.extract(response_data, "$.data.username")
        email = JSONPathHelper.extract(response_data, "$.data.email")

        assert username is not None
        assert email is not None
        assert "@" in email

class TestOrderAPI:
    """订单 API 测试"""

    BASE_URL = "https://api.example.com"

    @pytest.fixture
    def auth_token(self, login_response):
        """认证 token"""
        return JSONPathHelper.extract(login_response, "$.data.token")

    @pytest.fixture
    def create_order_response(self, auth_token):
        """创建订单响应"""
        url = f"{self.BASE_URL}/orders"
        headers = {"Authorization": f"Bearer {auth_token}"}
        data = {
            "product_id": 1001,
            "quantity": 2
        }
        response = requests.post(url, json=data, headers=headers)
        return response.json()

    @pytest.fixture
    def order_id(self, create_order_response):
        """订单 ID"""
        return JSONPathHelper.extract(create_order_response, "$.data.order_id")

    def test_create_order(self, create_order_response, order_id):
        """测试创建订单"""
        # 验证订单创建成功
        code = JSONPathHelper.extract(create_order_response, "$.code")
        assert code == 201

        # 验证订单 ID 存在
        assert order_id is not None

        # 提取订单总金额
        total = JSONPathHelper.extract(create_order_response, "$.data.total")
        assert total > 0

        # 提取订单状态
        status = JSONPathHelper.extract(create_order_response, "$.data.status")
        assert status == "pending"

        # 提取订单商品列表
        items = JSONPathHelper.extract_all(create_order_response, "$.data.items[*]")
        assert len(items) > 0

        # 提取所有商品 ID
        product_ids = JSONPathHelper.extract_all(
            create_order_response, 
            "$.data.items[*].product_id"
        )
        assert len(product_ids) == len(items)

    def test_get_order_detail(self, auth_token, order_id):
        """测试获取订单详情"""
        url = f"{self.BASE_URL}/orders/{order_id}"
        headers = {"Authorization": f"Bearer {auth_token}"}
        response = requests.get(url, headers=headers)

        assert response.status_code == 200
        response_data = response.json()

        # 验证订单 ID 匹配
        retrieved_order_id = JSONPathHelper.extract(response_data, "$.data.order_id")
        assert retrieved_order_id == order_id

        # 提取订单创建时间
        created_at = JSONPathHelper.extract(response_data, "$.data.created_at")
        assert created_at is not None

        # 提取订单更新时间
        updated_at = JSONPathHelper.extract(response_data, "$.data.updated_at")
        assert updated_at is not None

12. 总结

12.1 JSONPath 的核心优势

  1. 简洁直观:使用类似文件路径的语法,易于理解和编写
  2. 功能强大:支持通配符、切片、递归等高级功能
  3. 灵活性强:可以处理复杂的嵌套结构和数组数据
  4. 可维护性好:将数据提取逻辑与业务逻辑分离

12.2 在 Pytest 中使用 JSONPath 的要点

  1. 安装库:使用 jsonpath-ng
  2. 创建辅助函数:封装常用的提取逻辑
  3. 使用 Fixture:在 fixture 中提取数据,便于复用
  4. 错误处理:始终考虑字段不存在的情况
  5. 类型提示:使用类型提示提高代码可读性

12.3 学习路径建议

  1. 第一步:理解 JSONPath 基本语法和操作符
  2. 第二步:学习在 Python 中使用 jsonpath-ng
  3. 第三步:创建辅助函数和工具类
  4. 第四步:在 pytest fixture 中应用 JSONPath
  5. 第五步:在实际项目中实践和优化

12.4 实践建议

  1. 多练习:通过实际项目练习 JSONPath 的使用
  2. 多思考:思考如何让数据提取更加清晰和可维护
  3. 多总结:总结不同场景下的最佳实践
  4. 多参考:参考 JSONPath 官方文档和示例

希望这份详细的教程能够帮助你掌握 pytest 中的 JSONPath 使用!记住,实践是最好的老师,多写代码,多测试,你一定会越来越熟练的!


附录:JSONPath 表达式速查表

A.1 基本操作符

操作符 说明 示例
$ 根节点 $
. 子节点 $.user.name
[] 数组索引 $.users[0]
[*] 所有元素 $.users[*]
.. 递归下降 $..name
[start:end] 数组切片 $.users[0:3]

A.2 常用表达式示例

需求 JSONPath 表达式 说明
提取用户 ID $.data.user.id 提取单个值
提取所有用户名 $.data.users[*].name 提取数组中的所有值
提取第一个用户 $.data.users[0] 提取数组第一个元素
提取最后一个用户 $.data.users[-1] 提取数组最后一个元素
提取前三个用户 $.data.users[0:3] 数组切片
递归查找所有 email $..email 递归搜索
提取所有订单的商品 ID $.orders[*].items[*].product_id 嵌套数组

A.3 常见错误和解决方案

错误 原因 解决方案
ParseError JSONPath 语法错误 检查表达式语法,确保以 $ 开头
KeyError 字段不存在 使用安全提取函数,提供默认值
返回空列表 路径不匹配 检查 JSON 数据结构,确认路径正确
性能问题 重复编译表达式 缓存编译后的表达式

文档结束

发表评论