Pytest 的变量提取详解
1. 什么是变量提取
1.1 变量提取的概念
在 pytest 测试中,变量提取是指从各种数据源中获取数据,并在测试用例中使用的过程。这些数据源可以是:
- Fixture 返回值:从 pytest fixture 中获取数据
- API 响应数据:从 HTTP 请求的响应中提取数据
- 配置文件:从 YAML、JSON、INI 等配置文件中读取数据
- 环境变量:从系统环境变量中获取配置
- 测试数据文件:从 Excel、CSV、数据库等数据源中提取
- 参数化数据:从 pytest 的参数化装饰器中获取数据
1.2 为什么需要变量提取
在自动化测试中,变量提取非常重要,原因包括:
- 数据复用:避免重复定义相同的数据
- 数据驱动:将测试数据与测试逻辑分离
- 动态数据:获取运行时产生的动态数据(如 token、ID 等)
- 配置管理:统一管理测试配置和环境变量
- 可维护性:集中管理数据,便于修改和维护
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 变量提取的核心要点
- Fixture 是基础:Fixture 是 pytest 中变量提取的主要机制
- 作用域很重要:合理设置 fixture 的作用域可以提高测试效率
- 配置文件分离:将配置和数据从代码中分离,提高可维护性
- 链式提取:一个 fixture 可以依赖另一个 fixture,实现数据的链式提取
- 错误处理:始终考虑变量提取可能失败的情况
11.2 学习路径建议
- 第一步:掌握基本的 fixture 使用和变量提取
- 第二步:学习从 API 响应中提取变量
- 第三步:学习从配置文件中提取变量
- 第四步:掌握参数化测试中的变量提取
- 第五步:学习使用 conftest.py 共享变量
- 第六步:在实际项目中应用和优化
11.3 实践建议
- 多练习:通过实际项目练习变量提取
- 多思考:思考如何让变量提取更加清晰和可维护
- 多总结:总结不同场景下的最佳实践
- 多交流:与其他测试工程师交流经验
希望这份详细的教程能够帮助你掌握 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:对象序列化/反序列化
文档结束