18.pytest的变量提取

Pytest 的变量提取详解

1. 什么是变量提取

1.1 变量提取的概念

在 pytest 测试中,变量提取是指从各种数据源中获取数据,并在测试用例中使用的过程。这些数据源可以是:

  • Fixture 返回值:从 pytest fixture 中获取数据
  • API 响应数据:从 HTTP 请求的响应中提取数据
  • 配置文件:从 YAML、JSON、INI 等配置文件中读取数据
  • 环境变量:从系统环境变量中获取配置
  • 测试数据文件:从 Excel、CSV、数据库等数据源中提取
  • 参数化数据:从 pytest 的参数化装饰器中获取数据

1.2 为什么需要变量提取

在自动化测试中,变量提取非常重要,原因包括:

  1. 数据复用:避免重复定义相同的数据
  2. 数据驱动:将测试数据与测试逻辑分离
  3. 动态数据:获取运行时产生的动态数据(如 token、ID 等)
  4. 配置管理:统一管理测试配置和环境变量
  5. 可维护性:集中管理数据,便于修改和维护

1.3 变量提取的应用场景

  • API 测试:提取响应中的 token、用户 ID、订单号等
  • 数据库测试:提取查询结果中的数据
  • UI 测试:提取页面元素的值
  • 配置管理:提取不同环境的配置信息
  • 数据驱动测试:从外部文件读取测试数据

2. 从 Fixture 中提取变量

2.1 Fixture 基础回顾

Fixture 是 pytest 的核心功能之一,它可以提供测试所需的数据、对象或环境。Fixture 通过 @pytest.fixture 装饰器定义,并通过函数参数的方式注入到测试用例中。

2.2 基本 Fixture 变量提取

2.2.1 提取简单数据

最简单的变量提取就是从 fixture 的返回值中获取数据:

import pytest

# 定义一个 fixture,返回一个字符串
@pytest.fixture
def username():
    return "test_user"

# 在测试用例中使用 fixture
def test_login(username):
    # username 变量就是从 fixture 中提取的
    print(f"用户名: {username}")  # 输出: 用户名: test_user
    assert username == "test_user"

2.2.2 提取字典数据

Fixture 可以返回字典,我们可以直接使用整个字典,也可以提取其中的值:

import pytest

@pytest.fixture
def user_info():
    return {
        "username": "test_user",
        "password": "123456",
        "email": "test@example.com"
    }

# 方式一:使用整个字典
def test_user_info(user_info):
    assert user_info["username"] == "test_user"
    assert user_info["email"] == "test@example.com"

# 方式二:在 fixture 中解包字典
@pytest.fixture
def user_credentials(user_info):
    return user_info["username"], user_info["password"]

def test_login_with_credentials(user_credentials):
    username, password = user_credentials
    assert username == "test_user"
    assert password == "123456"

2.2.3 提取列表数据

import pytest

@pytest.fixture
def product_list():
    return ["苹果", "香蕉", "橙子", "葡萄"]

def test_product_count(product_list):
    # 提取列表长度
    count = len(product_list)
    assert count == 4

def test_first_product(product_list):
    # 提取第一个元素
    first_product = product_list[0]
    assert first_product == "苹果"

def test_all_products(product_list):
    # 遍历列表中的所有元素
    for product in product_list:
        assert isinstance(product, str)
        print(f"产品: {product}")

2.3 从多个 Fixture 中提取变量

一个测试用例可以使用多个 fixture,从每个 fixture 中提取不同的变量:

import pytest

@pytest.fixture
def base_url():
    return "https://api.example.com"

@pytest.fixture
def api_key():
    return "your_api_key_here"

@pytest.fixture
def headers():
    return {
        "Content-Type": "application/json",
        "User-Agent": "pytest-test"
    }

# 从多个 fixture 中提取变量
def test_api_request(base_url, api_key, headers):
    # 提取所有需要的变量
    url = base_url + "/users"
    key = api_key
    request_headers = headers

    # 使用提取的变量
    print(f"请求 URL: {url}")
    print(f"API Key: {key}")
    print(f"请求头: {request_headers}")

    # 在实际测试中,可以使用这些变量发送请求
    # response = requests.get(url, headers=request_headers, params={"key": key})

2.4 Fixture 依赖和变量传递

Fixture 可以依赖其他 fixture,实现变量的传递和组合:

import pytest

@pytest.fixture
def database_config():
    return {
        "host": "localhost",
        "port": 3306,
        "database": "test_db"
    }

@pytest.fixture
def db_connection_string(database_config):
    # 从 database_config fixture 中提取变量
    host = database_config["host"]
    port = database_config["port"]
    db = database_config["database"]

    # 组合成连接字符串
    return f"mysql://{host}:{port}/{db}"

def test_database_connection(db_connection_string):
    # 使用组合后的连接字符串
    connection_string = db_connection_string
    print(f"数据库连接字符串: {connection_string}")
    assert "mysql://localhost:3306/test_db" in connection_string

2.5 动态 Fixture 变量提取

Fixture 可以根据不同的条件返回不同的数据:

import pytest

@pytest.fixture
def environment(request):
    # 从命令行参数或标记中获取环境信息
    env = request.config.getoption("--env", default="test")
    return env

@pytest.fixture
def api_base_url(environment):
    # 根据环境提取不同的 base URL
    urls = {
        "dev": "https://dev-api.example.com",
        "test": "https://test-api.example.com",
        "prod": "https://api.example.com"
    }
    return urls.get(environment, urls["test"])

def test_api_endpoint(api_base_url):
    # 使用动态提取的 URL
    url = api_base_url
    print(f"API 基础 URL: {url}")
    assert url.startswith("https://")

3. 从 API 响应中提取变量

3.1 为什么需要从 API 响应中提取变量

在 API 测试中,经常需要:

  • 提取登录接口返回的 token,用于后续请求的认证
  • 提取创建接口返回的 ID,用于查询、更新、删除操作
  • 提取响应中的某些字段,用于验证其他接口

3.2 基本响应数据提取

3.2.1 提取 JSON 响应数据

import pytest
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

def test_extract_token(login_response):
    # 从响应中提取 token
    response_data = login_response.json()
    token = response_data["data"]["token"]

    print(f"提取的 Token: {token}")
    assert token is not None
    assert len(token) > 0

def test_extract_user_id(login_response):
    # 从响应中提取用户 ID
    response_data = login_response.json()
    user_id = response_data["data"]["user"]["id"]

    print(f"提取的用户 ID: {user_id}")
    assert isinstance(user_id, int)

3.2.2 提取嵌套的 JSON 数据

import pytest
import requests

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

def test_extract_nested_data(api_response):
    # 提取嵌套的数据
    user_id = api_response["data"]["user"]["id"]
    username = api_response["data"]["user"]["username"]
    email = api_response["data"]["user"]["profile"]["email"]
    token = api_response["data"]["token"]

    print(f"用户 ID: {user_id}")
    print(f"用户名: {username}")
    print(f"邮箱: {email}")
    print(f"Token: {token}")

    assert user_id == 12345
    assert username == "test_user"
    assert email == "test@example.com"

3.3 使用 Fixture 提取和存储响应数据

更好的做法是创建一个 fixture 来提取和存储响应数据:

import pytest
import requests

@pytest.fixture
def login_token():
    """登录并提取 token"""
    url = "https://api.example.com/login"
    data = {
        "username": "test_user",
        "password": "123456"
    }
    response = requests.post(url, json=data)
    response_data = response.json()

    # 提取 token
    token = response_data["data"]["token"]
    return token

@pytest.fixture
def user_info(login_token):
    """使用 token 获取用户信息"""
    url = "https://api.example.com/user/info"
    headers = {
        "Authorization": f"Bearer {login_token}"
    }
    response = requests.get(url, headers=headers)
    return response.json()

def test_use_extracted_token(login_token):
    # 使用提取的 token
    token = login_token
    print(f"使用 Token: {token}")

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

def test_use_user_info(user_info):
    # 使用提取的用户信息
    username = user_info["data"]["username"]
    email = user_info["data"]["email"]
    print(f"用户名: {username}, 邮箱: {email}")

3.4 提取多个变量并组合使用

import pytest
import requests

@pytest.fixture
def login_data():
    """登录并提取多个变量"""
    url = "https://api.example.com/login"
    data = {
        "username": "test_user",
        "password": "123456"
    }
    response = requests.post(url, json=data)
    response_data = response.json()

    # 提取多个变量
    token = response_data["data"]["token"]
    user_id = response_data["data"]["user_id"]
    expires_in = response_data["data"]["expires_in"]

    # 返回一个字典,包含所有提取的变量
    return {
        "token": token,
        "user_id": user_id,
        "expires_in": expires_in
    }

def test_create_order(login_data):
    # 从 fixture 中提取多个变量
    token = login_data["token"]
    user_id = login_data["user_id"]

    # 使用提取的变量创建订单
    url = "https://api.example.com/orders"
    headers = {"Authorization": f"Bearer {token}"}
    order_data = {
        "user_id": user_id,
        "product_id": 1001,
        "quantity": 2
    }

    # response = requests.post(url, json=order_data, headers=headers)
    print(f"使用 Token: {token}")
    print(f"使用用户 ID: {user_id}")

3.5 链式提取:一个接口的响应作为另一个接口的输入

import pytest
import requests

@pytest.fixture
def login_token():
    """第一步:登录并提取 token"""
    url = "https://api.example.com/login"
    data = {"username": "test_user", "password": "123456"}
    response = requests.post(url, json=data)
    return response.json()["data"]["token"]

@pytest.fixture
def order_id(login_token):
    """第二步:使用 token 创建订单,提取订单 ID"""
    url = "https://api.example.com/orders"
    headers = {"Authorization": f"Bearer {login_token}"}
    order_data = {"product_id": 1001, "quantity": 2}
    response = requests.post(url, json=order_data, headers=headers)
    return response.json()["data"]["order_id"]

@pytest.fixture
def payment_id(order_id, login_token):
    """第三步:使用订单 ID 和 token 创建支付,提取支付 ID"""
    url = "https://api.example.com/payments"
    headers = {"Authorization": f"Bearer {login_token}"}
    payment_data = {"order_id": order_id, "amount": 199.99}
    response = requests.post(url, json=payment_data, headers=headers)
    return response.json()["data"]["payment_id"]

def test_complete_flow(login_token, order_id, payment_id):
    """测试完整的流程,使用所有提取的变量"""
    print(f"Token: {login_token}")
    print(f"订单 ID: {order_id}")
    print(f"支付 ID: {payment_id}")

    # 验证支付状态
    url = f"https://api.example.com/payments/{payment_id}"
    headers = {"Authorization": f"Bearer {login_token}"}
    # response = requests.get(url, headers=headers)
    # assert response.json()["data"]["status"] == "paid"

4. 从配置文件中提取变量

4.1 从 YAML 配置文件中提取变量

4.1.1 读取 YAML 文件

首先,我们需要安装 PyYAML:

pip install pyyaml

4.1.2 基本 YAML 配置读取

创建一个配置文件 config.yaml

# config.yaml
database:
  host: localhost
  port: 3306
  username: test_user
  password: test_password
  database: test_db

api:
  base_url: https://api.example.com
  timeout: 30
  api_key: your_api_key_here

test_data:
  users:
    - username: user1
      password: pass1
    - username: user2
      password: pass2

读取配置文件的代码:

import pytest
import yaml
import os

@pytest.fixture(scope="session")
def config():
    """读取 YAML 配置文件"""
    config_path = os.path.join(os.path.dirname(__file__), "config.yaml")
    with open(config_path, "r", encoding="utf-8") as f:
        config_data = yaml.safe_load(f)
    return config_data

def test_database_config(config):
    # 从配置中提取数据库配置
    db_host = config["database"]["host"]
    db_port = config["database"]["port"]
    db_username = config["database"]["username"]
    db_password = config["database"]["password"]
    db_name = config["database"]["database"]

    print(f"数据库主机: {db_host}")
    print(f"数据库端口: {db_port}")
    print(f"用户名: {db_username}")
    print(f"密码: {db_password}")
    print(f"数据库名: {db_name}")

    assert db_host == "localhost"
    assert db_port == 3306

def test_api_config(config):
    # 从配置中提取 API 配置
    base_url = config["api"]["base_url"]
    timeout = config["api"]["timeout"]
    api_key = config["api"]["api_key"]

    print(f"API 基础 URL: {base_url}")
    print(f"超时时间: {timeout}")
    print(f"API Key: {api_key}")

    assert base_url == "https://api.example.com"
    assert timeout == 30

4.1.3 提取测试数据

import pytest
import yaml
import os

@pytest.fixture(scope="session")
def test_users(config):
    """从配置中提取测试用户数据"""
    return config["test_data"]["users"]

@pytest.mark.parametrize("user", [
    {"username": "user1", "password": "pass1"},
    {"username": "user2", "password": "pass2"}
])
def test_login_with_users(user):
    # 使用提取的用户数据
    username = user["username"]
    password = user["password"]

    print(f"测试登录 - 用户名: {username}, 密码: {password}")
    # 实际测试代码
    # response = requests.post(url, json={"username": username, "password": password})

4.2 从 JSON 配置文件中提取变量

创建配置文件 config.json

{
  "database": {
    "host": "localhost",
    "port": 3306,
    "username": "test_user",
    "password": "test_password"
  },
  "api": {
    "base_url": "https://api.example.com",
    "timeout": 30
  }
}

读取 JSON 配置文件:

import pytest
import json
import os

@pytest.fixture(scope="session")
def json_config():
    """读取 JSON 配置文件"""
    config_path = os.path.join(os.path.dirname(__file__), "config.json")
    with open(config_path, "r", encoding="utf-8") as f:
        config_data = json.load(f)
    return config_data

def test_json_config(json_config):
    # 从 JSON 配置中提取变量
    db_host = json_config["database"]["host"]
    api_url = json_config["api"]["base_url"]

    print(f"数据库主机: {db_host}")
    print(f"API URL: {api_url}")

    assert db_host == "localhost"
    assert api_url == "https://api.example.com"

4.3 从环境变量中提取变量

4.3.1 读取系统环境变量

import pytest
import os

@pytest.fixture(scope="session")
def env_config():
    """从环境变量中提取配置"""
    return {
        "database_url": os.getenv("DATABASE_URL", "localhost:3306"),
        "api_key": os.getenv("API_KEY", "default_key"),
        "environment": os.getenv("ENVIRONMENT", "test")
    }

def test_env_config(env_config):
    # 使用环境变量
    db_url = env_config["database_url"]
    api_key = env_config["api_key"]
    env = env_config["environment"]

    print(f"数据库 URL: {db_url}")
    print(f"API Key: {api_key}")
    print(f"环境: {env}")

4.3.2 使用 python-dotenv 读取 .env 文件

首先安装 python-dotenv:

pip install python-dotenv

创建 .env 文件:

DATABASE_URL=localhost:3306
API_KEY=your_secret_key_here
ENVIRONMENT=test
DEBUG=true

读取 .env 文件:

import pytest
import os
from dotenv import load_dotenv

# 加载 .env 文件
load_dotenv()

@pytest.fixture(scope="session")
def dotenv_config():
    """从 .env 文件中提取配置"""
    return {
        "database_url": os.getenv("DATABASE_URL"),
        "api_key": os.getenv("API_KEY"),
        "environment": os.getenv("ENVIRONMENT"),
        "debug": os.getenv("DEBUG", "false").lower() == "true"
    }

def test_dotenv_config(dotenv_config):
    # 使用从 .env 文件中提取的变量
    db_url = dotenv_config["database_url"]
    api_key = dotenv_config["api_key"]
    debug = dotenv_config["debug"]

    print(f"数据库 URL: {db_url}")
    print(f"API Key: {api_key}")
    print(f"调试模式: {debug}")

4.4 多环境配置提取

根据不同环境提取不同的配置:

import pytest
import yaml
import os

@pytest.fixture(scope="session")
def environment():
    """获取当前测试环境"""
    return os.getenv("TEST_ENV", "test")  # 默认是 test 环境

@pytest.fixture(scope="session")
def config(environment):
    """根据环境加载不同的配置文件"""
    config_file = f"config_{environment}.yaml"
    config_path = os.path.join(os.path.dirname(__file__), config_file)

    with open(config_path, "r", encoding="utf-8") as f:
        config_data = yaml.safe_load(f)

    return config_data

def test_multi_env_config(config, environment):
    # 根据环境提取配置
    base_url = config["api"]["base_url"]
    print(f"当前环境: {environment}")
    print(f"API 基础 URL: {base_url}")

    if environment == "dev":
        assert "dev" in base_url
    elif environment == "test":
        assert "test" in base_url
    elif environment == "prod":
        assert "api.example.com" in base_url

5. 从参数化测试中提取变量

5.1 基本参数化变量提取

pytest 的参数化功能可以让我们从参数列表中提取变量:

import pytest

# 基本参数化
@pytest.mark.parametrize("username,password", [
    ("user1", "pass1"),
    ("user2", "pass2"),
    ("user3", "pass3")
])
def test_login(username, password):
    # 从参数化中提取变量
    print(f"用户名: {username}, 密码: {password}")
    assert username is not None
    assert password is not None

5.2 从参数化字典中提取变量

import pytest

# 使用字典列表进行参数化
test_users = [
    {"username": "user1", "password": "pass1", "email": "user1@example.com"},
    {"username": "user2", "password": "pass2", "email": "user2@example.com"},
    {"username": "user3", "password": "pass3", "email": "user3@example.com"}
]

@pytest.mark.parametrize("user", test_users)
def test_user_info(user):
    # 从参数字典中提取变量
    username = user["username"]
    password = user["password"]
    email = user["email"]

    print(f"用户名: {username}")
    print(f"密码: {password}")
    print(f"邮箱: {email}")

    assert username.startswith("user")
    assert "@" in email

5.3 组合参数化和 Fixture

import pytest

@pytest.fixture
def base_url():
    return "https://api.example.com"

# 参数化 + Fixture
@pytest.mark.parametrize("endpoint,expected_status", [
    ("/users", 200),
    ("/products", 200),
    ("/orders", 200)
])
def test_api_endpoints(base_url, endpoint, expected_status):
    # 从 fixture 和参数化中提取变量
    url = base_url + endpoint
    status = expected_status

    print(f"测试 URL: {url}")
    print(f"期望状态码: {status}")

    # 实际测试代码
    # response = requests.get(url)
    # assert response.status_code == expected_status

5.4 从外部文件读取参数化数据

创建 test_data.yaml

test_cases:
  - username: user1
    password: pass1
    expected_result: success
  - username: user2
    password: pass2
    expected_result: success
  - username: invalid_user
    password: wrong_pass
    expected_result: failed

使用 YAML 文件进行参数化:

import pytest
import yaml
import os

def load_test_data():
    """加载测试数据"""
    data_path = os.path.join(os.path.dirname(__file__), "test_data.yaml")
    with open(data_path, "r", encoding="utf-8") as f:
        data = yaml.safe_load(f)
    return data["test_cases"]

# 从 YAML 文件中提取参数化数据
@pytest.mark.parametrize("test_case", load_test_data())
def test_login_with_data(test_case):
    # 从参数化数据中提取变量
    username = test_case["username"]
    password = test_case["password"]
    expected_result = test_case["expected_result"]

    print(f"测试用例 - 用户名: {username}, 期望结果: {expected_result}")

    # 执行测试
    # result = login(username, password)
    # assert result == expected_result

6. 使用 conftest.py 共享变量

6.1 conftest.py 的作用

conftest.py 是 pytest 的特殊文件,用于存放共享的 fixture 和配置。在 conftest.py 中定义的 fixture 可以被同一目录及其子目录中的所有测试文件使用。

6.2 在 conftest.py 中定义共享变量

创建 conftest.py

# conftest.py
import pytest
import yaml
import os

@pytest.fixture(scope="session")
def base_config():
    """共享的基础配置"""
    config_path = os.path.join(os.path.dirname(__file__), "config.yaml")
    with open(config_path, "r", encoding="utf-8") as f:
        config = yaml.safe_load(f)
    return config

@pytest.fixture(scope="session")
def api_base_url(base_config):
    """从配置中提取 API 基础 URL"""
    return base_config["api"]["base_url"]

@pytest.fixture(scope="session")
def database_config(base_config):
    """从配置中提取数据库配置"""
    return base_config["database"]

@pytest.fixture
def auth_token():
    """共享的认证 token"""
    # 这里可以是从登录接口获取的真实 token
    return "test_token_12345"

在任何测试文件中使用:

# test_api.py
def test_api_request(api_base_url, auth_token):
    # 使用 conftest.py 中定义的共享变量
    url = api_base_url + "/users"
    token = auth_token

    print(f"请求 URL: {url}")
    print(f"Token: {token}")

    # headers = {"Authorization": f"Bearer {token}"}
    # response = requests.get(url, headers=headers)

6.3 多级 conftest.py 的变量提取

项目结构:

project/
├── conftest.py          # 项目级配置
├── tests/
│   ├── conftest.py      # 测试目录级配置
│   ├── api/
│   │   ├── conftest.py  # API 测试专用配置
│   │   └── test_users.py
│   └── ui/
│       ├── conftest.py  # UI 测试专用配置
│       └── test_login.py

项目级 conftest.py

# project/conftest.py
import pytest

@pytest.fixture(scope="session")
def project_config():
    return {
        "project_name": "测试项目",
        "version": "1.0.0"
    }

测试目录级 conftest.py

# project/tests/conftest.py
import pytest

@pytest.fixture(scope="session")
def test_environment():
    return "test"

@pytest.fixture(scope="session")
def base_url(test_environment):
    urls = {
        "test": "https://test-api.example.com",
        "prod": "https://api.example.com"
    }
    return urls[test_environment]

API 测试专用 conftest.py

# project/tests/api/conftest.py
import pytest

@pytest.fixture
def api_headers(base_url):
    return {
        "Content-Type": "application/json",
        "X-API-Version": "v1"
    }

在测试文件中使用:

# project/tests/api/test_users.py
def test_get_users(base_url, api_headers):
    # base_url 来自 tests/conftest.py
    # api_headers 来自 tests/api/conftest.py
    url = base_url + "/users"
    headers = api_headers

    print(f"URL: {url}")
    print(f"Headers: {headers}")

7. 使用 pytest 的 request 对象提取变量

7.1 request 对象简介

pytest 的 request 对象提供了访问测试上下文信息的能力,可以获取测试名称、参数、标记等信息。

7.2 从 request 中提取测试信息

import pytest

@pytest.fixture
def test_info(request):
    """从 request 对象中提取测试信息"""
    return {
        "test_name": request.node.name,  # 测试函数名
        "test_file": request.node.fspath,  # 测试文件路径
        "test_markers": [mark.name for mark in request.node.iter_markers()],  # 测试标记
        "test_params": dict(request.node.funcargs)  # 测试参数
    }

def test_example(test_info):
    # 使用提取的测试信息
    print(f"测试名称: {test_info['test_name']}")
    print(f"测试文件: {test_info['test_file']}")
    print(f"测试标记: {test_info['test_markers']}")
    print(f"测试参数: {test_info['test_params']}")

7.3 从 request 中提取参数化值

import pytest

@pytest.fixture
def param_value(request):
    """提取参数化的值"""
    # 获取参数化的参数名和值
    if hasattr(request, "param"):
        return request.param
    return None

@pytest.mark.parametrize("param_value", ["value1", "value2", "value3"], indirect=True)
def test_with_param(param_value):
    # 使用提取的参数值
    print(f"参数值: {param_value}")
    assert param_value in ["value1", "value2", "value3"]

7.4 从 request 中提取配置选项

import pytest

@pytest.fixture
def test_config(request):
    """从命令行选项或配置中提取变量"""
    # 获取命令行选项
    env = request.config.getoption("--env", default="test")
    browser = request.config.getoption("--browser", default="chrome")

    return {
        "environment": env,
        "browser": browser
    }

def test_with_config(test_config):
    # 使用提取的配置
    env = test_config["environment"]
    browser = test_config["browser"]

    print(f"测试环境: {env}")
    print(f"浏览器: {browser}")

# 在 conftest.py 中添加命令行选项
def pytest_addoption(parser):
    parser.addoption("--env", action="store", default="test", help="测试环境")
    parser.addoption("--browser", action="store", default="chrome", help="浏览器类型")

8. 实际应用场景示例

8.1 场景一:API 测试中的 Token 提取和使用

import pytest
import requests

class TestAPIFlow:
    """演示 API 测试中变量提取的完整流程"""

    @pytest.fixture(scope="class")
    def login_token(self):
        """登录并提取 token"""
        url = "https://api.example.com/login"
        data = {"username": "test_user", "password": "123456"}
        response = requests.post(url, json=data)
        token = response.json()["data"]["token"]
        return token

    @pytest.fixture(scope="class")
    def user_id(self, login_token):
        """使用 token 获取用户 ID"""
        url = "https://api.example.com/user/info"
        headers = {"Authorization": f"Bearer {login_token}"}
        response = requests.get(url, headers=headers)
        user_id = response.json()["data"]["id"]
        return user_id

    def test_get_user_info(self, login_token, user_id):
        """测试获取用户信息"""
        url = f"https://api.example.com/users/{user_id}"
        headers = {"Authorization": f"Bearer {login_token}"}
        response = requests.get(url, headers=headers)

        assert response.status_code == 200
        assert response.json()["data"]["id"] == user_id

    def test_create_order(self, login_token, user_id):
        """测试创建订单"""
        url = "https://api.example.com/orders"
        headers = {"Authorization": f"Bearer {login_token}"}
        data = {"user_id": user_id, "product_id": 1001, "quantity": 2}
        response = requests.post(url, json=data, headers=headers)

        assert response.status_code == 201
        order_id = response.json()["data"]["order_id"]

        # 提取订单 ID 用于后续测试
        return order_id

8.2 场景二:数据驱动测试中的变量提取

import pytest
import yaml
import os

class TestDataDriven:
    """演示数据驱动测试中的变量提取"""

    @pytest.fixture(scope="class")
    def test_data(self):
        """从 YAML 文件加载测试数据"""
        data_path = os.path.join(os.path.dirname(__file__), "test_data.yaml")
        with open(data_path, "r", encoding="utf-8") as f:
            data = yaml.safe_load(f)
        return data

    @pytest.mark.parametrize("test_case", [
        {"username": "user1", "password": "pass1", "expected": "success"},
        {"username": "user2", "password": "pass2", "expected": "success"},
        {"username": "invalid", "password": "wrong", "expected": "failed"}
    ])
    def test_login_scenarios(self, test_case, test_data):
        """使用参数化和配置文件中的数据进行测试"""
        # 从参数化中提取变量
        username = test_case["username"]
        password = test_case["password"]
        expected = test_case["expected"]

        # 从配置文件中提取变量
        api_url = test_data["api"]["base_url"]
        login_endpoint = test_data["api"]["endpoints"]["login"]

        # 组合使用
        full_url = api_url + login_endpoint
        login_data = {"username": username, "password": password}

        print(f"测试 URL: {full_url}")
        print(f"用户名: {username}, 期望结果: {expected}")

        # 实际测试代码
        # response = requests.post(full_url, json=login_data)
        # assert response.json()["status"] == expected

8.3 场景三:多环境配置的变量提取

import pytest
import yaml
import os

class TestMultiEnvironment:
    """演示多环境配置中的变量提取"""

    @pytest.fixture(scope="session")
    def environment(self):
        """获取当前环境"""
        return os.getenv("TEST_ENV", "test")

    @pytest.fixture(scope="session")
    def env_config(self, environment):
        """根据环境加载配置"""
        config_file = f"config_{environment}.yaml"
        config_path = os.path.join(os.path.dirname(__file__), config_file)

        with open(config_path, "r", encoding="utf-8") as f:
            config = yaml.safe_load(f)

        return config

    @pytest.fixture(scope="session")
    def api_base_url(self, env_config):
        """提取 API 基础 URL"""
        return env_config["api"]["base_url"]

    @pytest.fixture(scope="session")
    def database_config(self, env_config):
        """提取数据库配置"""
        return env_config["database"]

    def test_api_request(self, api_base_url):
        """使用提取的 API URL"""
        url = api_base_url + "/users"
        print(f"请求 URL: {url}")
        # response = requests.get(url)

    def test_database_connection(self, database_config):
        """使用提取的数据库配置"""
        host = database_config["host"]
        port = database_config["port"]
        print(f"数据库: {host}:{port}")
        # connection = connect(host, port)

9. 变量提取的最佳实践

9.1 使用有意义的变量名

# ❌ 不好的做法
@pytest.fixture
def data():
    return {"token": "abc123"}

# ✅ 好的做法
@pytest.fixture
def auth_token():
    return "abc123"

@pytest.fixture
def user_credentials():
    return {"username": "test_user", "password": "123456"}

9.2 合理设置 Fixture 的作用域

import pytest

# Session 级别:整个测试会话只执行一次,适合配置、数据库连接等
@pytest.fixture(scope="session")
def database_connection():
    # 建立数据库连接
    return connection

# Class 级别:每个测试类只执行一次,适合类级别的设置
@pytest.fixture(scope="class")
def login_token():
    # 登录获取 token
    return token

# Function 级别:每个测试函数都执行,适合需要重置的数据
@pytest.fixture(scope="function")
def clean_data():
    # 清理测试数据
    return []

9.3 使用类型提示提高可读性

from typing import Dict, List

@pytest.fixture
def user_info() -> Dict[str, str]:
    """返回用户信息字典"""
    return {
        "username": "test_user",
        "email": "test@example.com"
    }

@pytest.fixture
def user_list() -> List[Dict[str, str]]:
    """返回用户列表"""
    return [
        {"username": "user1", "email": "user1@example.com"},
        {"username": "user2", "email": "user2@example.com"}
    ]

def test_user_info(user_info: Dict[str, str]):
    # 使用类型提示,IDE 可以提供更好的代码补全
    username = user_info["username"]
    email = user_info["email"]

9.4 错误处理和默认值

import pytest
import os

@pytest.fixture
def api_key():
    """提取 API Key,如果不存在则使用默认值"""
    api_key = os.getenv("API_KEY")
    if not api_key:
        # 使用默认值或抛出异常
        pytest.skip("API_KEY 环境变量未设置")
    return api_key

@pytest.fixture
def config_with_defaults():
    """从配置文件提取变量,提供默认值"""
    import yaml
    try:
        with open("config.yaml", "r") as f:
            config = yaml.safe_load(f)
    except FileNotFoundError:
        # 使用默认配置
        config = {
            "api": {"base_url": "https://api.example.com"},
            "timeout": 30
        }
    return config

9.5 文档字符串和注释

import pytest

@pytest.fixture
def login_token():
    """
    登录并提取认证 token

    返回:
        str: 认证 token,用于后续 API 请求的 Authorization header

    示例:
        >>> token = login_token()
        >>> headers = {"Authorization": f"Bearer {token}"}
    """
    url = "https://api.example.com/login"
    data = {"username": "test_user", "password": "123456"}
    response = requests.post(url, json=data)
    return response.json()["data"]["token"]

9.6 避免硬编码

# ❌ 不好的做法:硬编码
def test_api():
    url = "https://api.example.com/users"
    api_key = "hardcoded_key_12345"
    headers = {"Authorization": f"Bearer {api_key}"}

# ✅ 好的做法:从配置中提取
@pytest.fixture
def api_config():
    import yaml
    with open("config.yaml", "r") as f:
        return yaml.safe_load(f)

def test_api(api_config):
    url = api_config["api"]["base_url"] + "/users"
    api_key = api_config["api"]["key"]
    headers = {"Authorization": f"Bearer {api_key}"}

10. 常见问题和解决方案

10.1 问题一:如何提取嵌套很深的 JSON 数据?

问题描述:响应数据嵌套很深,提取起来很麻烦。

解决方案:使用辅助函数或库来简化提取。

import pytest
from typing import Any, Dict

def extract_nested_value(data: Dict, path: str, default: Any = None) -> Any:
    """
    从嵌套字典中提取值

    参数:
        data: 嵌套字典
        path: 路径,使用点号分隔,如 "data.user.profile.email"
        default: 默认值

    返回:
        提取的值或默认值
    """
    keys = path.split(".")
    value = data
    for key in keys:
        if isinstance(value, dict) and key in value:
            value = value[key]
        else:
            return default
    return value

@pytest.fixture
def api_response():
    return {
        "data": {
            "user": {
                "profile": {
                    "email": "test@example.com"
                }
            }
        }
    }

def test_extract_nested(api_response):
    # 使用辅助函数提取嵌套值
    email = extract_nested_value(api_response, "data.user.profile.email")
    assert email == "test@example.com"

    # 或者使用 JSONPath(需要安装 jsonpath-ng)
    # from jsonpath_ng import parse
    # jsonpath_expr = parse("$.data.user.profile.email")
    # matches = [match.value for match in jsonpath_expr.find(api_response)]
    # email = matches[0] if matches else None

10.2 问题二:如何在多个测试之间共享提取的变量?

问题描述:一个测试中提取的变量需要在其他测试中使用。

解决方案:使用 session 或 class 级别的 fixture。

import pytest

@pytest.fixture(scope="session")
def shared_data():
    """Session 级别的共享数据"""
    data = {}

    # 在 fixture 中提取和存储数据
    token = login_and_get_token()
    data["token"] = token

    user_id = get_user_id(token)
    data["user_id"] = user_id

    return data

def test_one(shared_data):
    # 使用共享的数据
    token = shared_data["token"]
    print(f"Test 1 使用 Token: {token}")

def test_two(shared_data):
    # 使用共享的数据
    user_id = shared_data["user_id"]
    print(f"Test 2 使用 User ID: {user_id}")

10.3 问题三:如何从响应中提取多个变量并分别使用?

问题描述:一个 API 响应包含多个需要提取的变量。

解决方案:创建多个 fixture 或返回字典。

import pytest

# 方案一:返回字典
@pytest.fixture
def login_response_data():
    response = login_api()
    return {
        "token": response["data"]["token"],
        "user_id": response["data"]["user_id"],
        "expires_in": response["data"]["expires_in"]
    }

def test_use_multiple_vars(login_response_data):
    token = login_response_data["token"]
    user_id = login_response_data["user_id"]
    expires_in = login_response_data["expires_in"]

    # 使用所有变量
    print(f"Token: {token}, User ID: {user_id}, Expires: {expires_in}")

# 方案二:创建多个独立的 fixture
@pytest.fixture
def auth_token():
    response = login_api()
    return response["data"]["token"]

@pytest.fixture
def user_id(auth_token):
    response = get_user_info(auth_token)
    return response["data"]["id"]

def test_use_separate_fixtures(auth_token, user_id):
    # 使用独立的 fixture
    print(f"Token: {auth_token}, User ID: {user_id}")

10.4 问题四:如何根据条件提取不同的变量?

问题描述:需要根据不同的条件提取不同的变量值。

解决方案:在 fixture 中使用条件判断。

import pytest

@pytest.fixture
def environment():
    return os.getenv("ENV", "test")

@pytest.fixture
def api_config(environment):
    """根据环境提取不同的配置"""
    configs = {
        "dev": {
            "base_url": "https://dev-api.example.com",
            "timeout": 10
        },
        "test": {
            "base_url": "https://test-api.example.com",
            "timeout": 30
        },
        "prod": {
            "base_url": "https://api.example.com",
            "timeout": 60
        }
    }
    return configs.get(environment, configs["test"])

def test_with_conditional_config(api_config):
    base_url = api_config["base_url"]
    timeout = api_config["timeout"]
    print(f"Base URL: {base_url}, Timeout: {timeout}")

10.5 问题五:提取的变量为空或不存在怎么办?

问题描述:从响应或配置中提取的变量可能为空或不存在。

解决方案:添加验证和默认值处理。

import pytest

@pytest.fixture
def safe_extract_token():
    """安全地提取 token,处理异常情况"""
    try:
        response = login_api()
        token = response.get("data", {}).get("token")

        if not token:
            pytest.skip("Token 提取失败")

        return token
    except Exception as e:
        pytest.skip(f"登录失败: {str(e)}")

@pytest.fixture
def config_with_validation():
    """提取配置并验证"""
    import yaml
    with open("config.yaml", "r") as f:
        config = yaml.safe_load(f)

    # 验证必需的配置项
    required_keys = ["api.base_url", "api.timeout"]
    for key in required_keys:
        keys = key.split(".")
        value = config
        for k in keys:
            if k not in value:
                raise ValueError(f"配置文件中缺少必需的键: {key}")
            value = value[k]

    return config

11. 总结

11.1 变量提取的核心要点

  1. Fixture 是基础:Fixture 是 pytest 中变量提取的主要机制
  2. 作用域很重要:合理设置 fixture 的作用域可以提高测试效率
  3. 配置文件分离:将配置和数据从代码中分离,提高可维护性
  4. 链式提取:一个 fixture 可以依赖另一个 fixture,实现数据的链式提取
  5. 错误处理:始终考虑变量提取可能失败的情况

11.2 学习路径建议

  1. 第一步:掌握基本的 fixture 使用和变量提取
  2. 第二步:学习从 API 响应中提取变量
  3. 第三步:学习从配置文件中提取变量
  4. 第四步:掌握参数化测试中的变量提取
  5. 第五步:学习使用 conftest.py 共享变量
  6. 第六步:在实际项目中应用和优化

11.3 实践建议

  1. 多练习:通过实际项目练习变量提取
  2. 多思考:思考如何让变量提取更加清晰和可维护
  3. 多总结:总结不同场景下的最佳实践
  4. 多交流:与其他测试工程师交流经验

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


附录:常用工具和库

A.1 JSONPath 提取工具

pip install jsonpath-ng
from jsonpath_ng import parse

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

# 使用 JSONPath 提取
jsonpath_expr = parse("$.users[*].name")
matches = [match.value for match in jsonpath_expr.find(data)]
print(matches)  # ['Alice', 'Bob']

A.2 配置文件读取库

  • PyYAML:读取 YAML 文件
  • configparser:读取 INI 文件(Python 标准库)
  • python-dotenv:读取 .env 文件
  • toml:读取 TOML 文件

A.3 数据验证库

  • pydantic:数据验证和设置管理
  • cerberus:数据验证库
  • marshmallow:对象序列化/反序列化

文档结束

发表评论