pytest详解
文章目录
- pytest详解
-
- [一、Pytest 语法概览](#一、Pytest 语法概览)
-
- [1. **基本测试结构**](#1. 基本测试结构)
- [2. **断言语法**](#2. 断言语法)
- [3. **参数化测试**](#3. 参数化测试)
- [二、配置文件 pytest.ini](#二、配置文件 pytest.ini)
-
- [1. 基本配置](#1. 基本配置)
- [2. 环境特定配置](#2. 环境特定配置)
- [三、pytest.main() 函数详解](#三、pytest.main() 函数详解)
-
- [1. **基本用法**](#1. 基本用法)
- [2. **高级配置**](#2. 高级配置)
- [3. **插件集成**](#3. 插件集成)
- [四、YAML 用例配置](#四、YAML 用例配置)
-
- [1. **测试数据配置**](#1. 测试数据配置)
- [2. **YAML 加载器**](#2. YAML 加载器)
- [3. **参数化使用**](#3. 参数化使用)
- 五、请求和响应校验
-
- [1. **Request 封装**](#1. Request 封装)
- [2. **Response 校验器**](#2. Response 校验器)
- [3. **测试中使用**](#3. 测试中使用)
- [六、conftest.py 作用详解](#六、conftest.py 作用详解)
-
- [1. **conftest.py 结构**](#1. conftest.py 结构)
- [2. **conftest.py 层级结构**](#2. conftest.py 层级结构)
- [七、Pytest 架构图 (Mermaid)](#七、Pytest 架构图 (Mermaid))
- [八、Pytest 工作流程图](#八、Pytest 工作流程图)
- 九、最佳实践总结
-
- [1. **项目结构**](#1. 项目结构)
- [2. **常用命令**](#2. 常用命令)
- [3. **实用技巧**](#3. 实用技巧)
一、Pytest 语法概览
pytest是一个基于Python的测试框架,它使用简单的assert语句来进行断言,并且具有丰富的插件系统。其语法特点如下:
- 测试文件以
test_开头或_test结尾。 - 测试类以
Test开头,且不能有__init__方法。 - 测试函数以
test_开头。 - 使用assert进行断言,例如:
assert 1 == 1。
python
# test_example.py
def test_addition():
assert 1 + 1 == 2
class TestMath:
def test_multiplication(self):
assert 2 * 2 == 4
1. 基本测试结构
python
# test_sample.py
def test_addition():
"""基本测试函数"""
assert 1 + 1 == 2
class TestMathOperations:
"""测试类"""
def test_multiplication(self):
assert 2 * 3 == 6
def test_division(self):
assert 6 / 2 == 3
2. 断言语法
python
def test_assertions():
# 基本断言
assert value == expected
assert value != unexpected
# 异常断言
with pytest.raises(ValueError):
int("abc")
# 警告断言
with pytest.warns(UserWarning):
warnings.warn("test", UserWarning)
# 近似相等
assert 0.1 + 0.2 == pytest.approx(0.3)
3. 参数化测试
python
import pytest
@pytest.mark.parametrize("input,expected", [
(1, 2),
(2, 3),
(3, 4)
])
def test_increment(input, expected):
assert input + 1 == expected
# 多参数组合
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [10, 20])
def test_multiply(x, y):
assert x * y == x * y
二、配置文件 pytest.ini
pytest.ini是pytest的配置文件,用于设置pytest的执行参数。它可以放在项目根目录下,影响该目录及其子目录中的测试。
示例pytest.ini文件:
ini
[pytest]
addopts = -v -s
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
fast: marks tests as fast
参数说明:
addopts:默认命令行参数,例如-v表示详细输出,-s表示输出打印信息。testpaths:指定测试目录。python_files:指定测试文件命名模式。python_classes:指定测试类命名模式。python_functions:指定测试函数命名模式。markers:自定义标记,用于分组测试。
1. 基本配置
ini
[pytest]
# 默认命令行选项
addopts =
-v # 详细输出
--tb=short # 简短的traceback
--strict-markers
--disable-warnings
--html=reports/report.html
--self-contained-html
# 文件匹配模式
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*
# 测试目录
testpaths =
tests
integration_tests
e2e_tests
# 标记定义
markers =
smoke: 冒烟测试
regression: 回归测试
slow: 慢速测试
api: API接口测试
db: 数据库测试
# 忽略目录
norecursedirs =
.git
.venv
node_modules
__pycache__
# 日志配置
log_format = %(asctime)s [%(levelname)s] %(message)s
log_date_format = %Y-%m-%d %H:%M:%S
log_cli = true
log_cli_level = INFO
# 自定义配置
minversion = 6.0
xfail_strict = true
2. 环境特定配置
ini
# pytest.ini.dev (开发环境)
[pytest]
addopts = -v -x --lf --tb=short
# pytest.ini.ci (持续集成环境)
[pytest]
addopts = -v --junitxml=junit.xml --tb=line
三、pytest.main() 函数详解
pytest.main()是运行测试的入口函数,可以在Python脚本中调用以运行测试。它接受一个参数列表,类似于命令行参数。
示例run_test.py:
python
import pytest
if __name__ == "__main__":
# 运行所有测试
pytest.main(["-v", "tests/"])
# 运行特定标记的测试
# pytest.main(["-m", "slow"])
# 运行特定文件
# pytest.main(["tests/test_example.py"])
1. 基本用法
python
# run_tests.py
import pytest
if __name__ == "__main__":
# 方式1: 传递参数列表
pytest.main(["-v", "tests/"])
# 方式2: 使用字符串
pytest.main("-v tests/")
# 方式3: 退出代码处理
exit_code = pytest.main(["-x", "tests/test_api.py"])
if exit_code != 0:
print("测试失败!")
exit(exit_code)
2. 高级配置
python
import sys
import pytest
def run_tests():
"""自定义测试运行器"""
# 动态添加路径
sys.path.insert(0, "src")
# 运行选项
args = [
"-v", # 详细模式
"--tb=short", # 简短回溯
"--maxfail=3", # 最多失败数
"--junitxml=results.xml",# JUnit报告
"--html=report.html", # HTML报告
"--self-contained-html",
"--cov=src", # 覆盖率
"--cov-report=html",
"-m", "not slow", # 排除慢测试
"tests/",
]
# 运行测试
return pytest.main(args)
if __name__ == "__main__":
result = run_tests()
sys.exit(result)
3. 插件集成
python
import pytest
def run_with_plugins():
"""使用插件运行"""
plugins = [
"pytest-html",
"pytest-cov",
"pytest-xdist",
"pytest-timeout"
]
return pytest.main([
"-v",
"-n", "auto", # 多进程
"--timeout=30",
"tests/"
])
四、YAML 用例配置
在测试中,我们有时会使用YAML文件来管理测试数据。YAML格式易于读写,可以存储复杂的数据结构。
示例test_data.yml:
yaml
- name: "test login success"
url: "/login"
method: "POST"
data:
username: "admin"
password: "123456"
expected:
code: 200
message: "login success"
- name: "test login failure"
url: "/login"
method: "POST"
data:
username: "admin"
password: "wrong"
expected:
code: 401
message: "login failed"
在测试中读取YAML文件:
python
import yaml
import pytest
def load_test_data(file_path):
with open(file_path, 'r') as f:
return yaml.safe_load(f)
@pytest.mark.parametrize("test_case", load_test_data("test_data.yml"))
def test_login(test_case):
# 使用test_case中的数据进行测试
pass
1. 测试数据配置
yaml
# test_data/api_tests.yml
api_tests:
login:
name: "用户登录接口测试"
base_url: "https://api.example.com"
endpoint: "/api/v1/login"
method: "POST"
headers:
Content-Type: "application/json"
test_cases:
- case_id: "TC_LOGIN_001"
name: "正常登录"
data:
username: "testuser"
password: "password123"
expected:
status_code: 200
response:
code: 0
message: "登录成功"
data:
token: !!str # 类型验证
- case_id: "TC_LOGIN_002"
name: "密码错误"
data:
username: "testuser"
password: "wrong"
expected:
status_code: 200
response:
code: 1001
message: "密码错误"
2. YAML 加载器
python
# utils/yaml_loader.py
import yaml
import os
from typing import Dict, Any
class YamlTestLoader:
@staticmethod
def load_test_cases(file_path: str) -> Dict[str, Any]:
"""加载YAML测试用例"""
with open(file_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
@staticmethod
def load_multiple_files(directory: str) -> Dict:
"""加载目录下所有YAML文件"""
test_cases = {}
for file in os.listdir(directory):
if file.endswith(('.yml', '.yaml')):
file_path = os.path.join(directory, file)
test_name = os.path.splitext(file)[0]
test_cases[test_name] = YamlTestLoader.load_test_cases(file_path)
return test_cases
# 使用示例
test_data = YamlTestLoader.load_test_cases("test_data/api_tests.yml")
3. 参数化使用
python
import pytest
from utils.yaml_loader import YamlTestLoader
# 加载测试数据
test_cases = YamlTestLoader.load_test_cases("test_data/api_tests.yml")
class TestAPI:
@pytest.mark.parametrize("test_case", test_cases["api_tests"]["login"]["test_cases"])
def test_login(self, test_case):
"""使用YAML数据驱动的测试"""
# 准备请求
request_data = test_case["data"]
# 发送请求
response = self.make_request(
method="POST",
url=test_case.get("base_url", "") + test_case["endpoint"],
data=request_data
)
# 验证响应
assert response.status_code == test_case["expected"]["status_code"]
assert response.json()["code"] == test_case["expected"]["response"]["code"]
五、请求和响应校验
在接口测试中,我们经常使用requests库发送HTTP请求,并对响应进行校验。
python
import requests
def test_request():
url = "http://httpbin.org/get"
response = requests.get(url)
# 校验状态码
assert response.status_code == 200
# 校验响应头
assert response.headers["Content-Type"] == "application/json"
# 校验响应体
json_data = response.json()
assert json_data["url"] == url
1. Request 封装
python
# core/request_client.py
import requests
import json
from typing import Dict, Any, Optional
class RequestClient:
def __init__(self, base_url: str = ""):
self.base_url = base_url
self.session = requests.Session()
self.default_headers = {
"Content-Type": "application/json",
"User-Agent": "Pytest-API-Test"
}
def request(
self,
method: str,
endpoint: str,
params: Optional[Dict] = None,
data: Optional[Dict] = None,
json_data: Optional[Dict] = None,
headers: Optional[Dict] = None,
timeout: int = 30
) -> requests.Response:
"""发送请求"""
# 合并headers
final_headers = {**self.default_headers, **(headers or {})}
# 构建URL
url = f"{self.base_url}{endpoint}"
# 发送请求
response = self.session.request(
method=method.upper(),
url=url,
params=params,
data=data,
json=json_data,
headers=final_headers,
timeout=timeout
)
return response
# 快捷方法
def get(self, endpoint: str, **kwargs):
return self.request("GET", endpoint, **kwargs)
def post(self, endpoint: str, **kwargs):
return self.request("POST", endpoint, **kwargs)
def put(self, endpoint: str, **kwargs):
return self.request("PUT", endpoint, **kwargs)
def delete(self, endpoint: str, **kwargs):
return self.request("DELETE", endpoint, **kwargs)
2. Response 校验器
python
# core/response_validator.py
import json
import jsonschema
from typing import Dict, Any, List
from pydantic import BaseModel, ValidationError
class ResponseValidator:
@staticmethod
def validate_status_code(response, expected_code: int):
"""验证状态码"""
assert response.status_code == expected_code, \
f"期望状态码 {expected_code}, 实际 {response.status_code}"
@staticmethod
def validate_json_schema(response, schema: Dict):
"""验证JSON Schema"""
try:
jsonschema.validate(
instance=response.json(),
schema=schema
)
except jsonschema.ValidationError as e:
raise AssertionError(f"Schema验证失败: {e.message}")
@staticmethod
def validate_pydantic_model(response, model_class: BaseModel):
"""使用Pydantic模型验证"""
try:
model_class(**response.json())
except ValidationError as e:
raise AssertionError(f"模型验证失败: {e}")
@staticmethod
def validate_response_time(response, max_time: float):
"""验证响应时间"""
assert response.elapsed.total_seconds() <= max_time, \
f"响应时间 {response.elapsed.total_seconds()}s 超过限制 {max_time}s"
@staticmethod
def validate_headers(response, expected_headers: Dict):
"""验证响应头"""
for key, value in expected_headers.items():
assert key in response.headers, f"缺少响应头: {key}"
assert response.headers[key] == value, \
f"响应头 {key} 不匹配: 期望 {value}, 实际 {response.headers[key]}"
class ComprehensiveValidator:
"""综合校验器"""
def __init__(self):
self.validations = []
def add_validation(self, validation_func, *args, **kwargs):
"""添加校验规则"""
self.validations.append((validation_func, args, kwargs))
return self
def run_all(self, response):
"""运行所有校验"""
errors = []
for func, args, kwargs in self.validations:
try:
func(response, *args, **kwargs)
except AssertionError as e:
errors.append(str(e))
if errors:
raise AssertionError("\n".join(errors))
3. 测试中使用
python
import pytest
from core.request_client import RequestClient
from core.response_validator import ResponseValidator, ComprehensiveValidator
class TestAPIIntegration:
@pytest.fixture
def client(self):
return RequestClient(base_url="https://api.example.com")
def test_user_api(self, client):
"""完整API测试示例"""
# 1. 发送请求
response = client.post("/api/users", json_data={
"name": "John",
"email": "john@example.com"
})
# 2. 使用综合校验器
validator = ComprehensiveValidator()
validator.add_validation(
ResponseValidator.validate_status_code,
expected_code=201
)
validator.add_validation(
ResponseValidator.validate_json_schema,
schema={
"type": "object",
"properties": {
"id": {"type": "integer"},
"name": {"type": "string"},
"email": {"type": "string", "format": "email"}
},
"required": ["id", "name", "email"]
}
)
validator.add_validation(
ResponseValidator.validate_response_time,
max_time=2.0
)
validator.run_all(response)
# 3. 业务逻辑验证
response_data = response.json()
assert response_data["name"] == "John"
assert "@" in response_data["email"]
六、conftest.py 作用详解
conftest.py是pytest的本地插件,用于定义钩子函数和夹具(fixture)。该文件可以放在项目根目录或任何测试子目录中,作用范围为其所在目录及其子目录。
常用功能:
- 定义夹具(fixture)供多个测试文件使用。
- 定义钩子函数,例如修改测试收集过程。
示例conftest.py:
python
import pytest
@pytest.fixture
def login():
return "token"
@pytest.fixture
def setup_database():
# 设置数据库
yield
# 清理数据库
# 钩子函数示例
def pytest_runtest_setup(item):
# 在每个测试运行前执行
pass
1. conftest.py 结构
python
# conftest.py
import pytest
import os
from typing import Dict, Generator
from core.request_client import RequestClient
# 配置hooks
def pytest_configure(config):
"""在测试运行前配置"""
config.addinivalue_line(
"markers", "integration: 集成测试"
)
def pytest_collection_modifyitems(items):
"""修改收集的测试项"""
for item in items:
# 自动添加标记
if "api" in item.nodeid:
item.add_marker(pytest.mark.api)
# Fixtures定义
@pytest.fixture(scope="session")
def config() -> Dict:
"""读取配置"""
import yaml
with open("config/test_config.yaml") as f:
return yaml.safe_load(f)
@pytest.fixture(scope="session")
def api_client(config) -> Generator:
"""API客户端"""
client = RequestClient(base_url=config["base_url"])
yield client
client.session.close()
@pytest.fixture
def auth_client(api_client, config):
"""认证客户端"""
# 登录获取token
response = api_client.post("/login", json_data={
"username": config["test_user"],
"password": config["test_password"]
})
token = response.json()["token"]
# 设置认证头
api_client.default_headers["Authorization"] = f"Bearer {token}"
return api_client
@pytest.fixture
def db_connection():
"""数据库连接"""
import psycopg2
conn = psycopg2.connect(
host="localhost",
database="testdb",
user="test"
)
yield conn
conn.close()
# 自定义fixture
@pytest.fixture
def create_test_user(db_connection):
"""创建测试用户"""
def _create(username: str, email: str):
cursor = db_connection.cursor()
cursor.execute(
"INSERT INTO users (username, email) VALUES (%s, %s) RETURNING id",
(username, email)
)
user_id = cursor.fetchone()[0]
db_connection.commit()
return user_id
yield _create
# 清理
cursor = db_connection.cursor()
cursor.execute("DELETE FROM users WHERE username LIKE 'test_%'")
db_connection.commit()
# 自定义钩子
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""获取测试结果"""
outcome = yield
rep = outcome.get_result()
if rep.when == "call" and rep.failed:
# 失败时截图(UI测试)
if hasattr(item, "funcargs") and "driver" in item.funcargs:
driver = item.funcargs["driver"]
screenshot_path = f"screenshots/{item.name}.png"
driver.save_screenshot(screenshot_path)
2. conftest.py 层级结构
text
project/
├── conftest.py # 全局fixtures
├── tests/
│ ├── conftest.py # 测试目录级fixtures
│ ├── api/
│ │ ├── conftest.py # API测试专用fixtures
│ │ └── test_user_api.py
│ └── ui/
│ ├── conftest.py # UI测试专用fixtures
│ └── test_login.py
└── config/
└── test_config.yaml
七、Pytest 架构图 (Mermaid)
测试执行流程
测试组织
Fixture 系统
插件系统
Pytest 核心框架
pytest.ini 配置
pytest.main 入口
测试发现
测试执行引擎
结果报告生成
pytest-html
pytest-cov
pytest-xdist
自定义插件
conftest.py
Fixture 注册表
测试依赖注入
测试文件 .py
测试数据 .yaml/.json
测试标记 markers
参数化 parametrize
测试用例实例
Request/Response
断言校验
测试结果
八、Pytest 工作流程图
Plugin Manager Reporter Test Executor Fixture Manager Test Discoverer Config Loader Test Runner Plugin Manager Reporter Test Executor Fixture Manager Test Discoverer Config Loader Test Runner 测试执行流程 alt [测试通过] [测试失败] [测试跳过] loop [每个测试用例] 加载 pytest.ini 配置 返回配置参数 发现测试用例 扫描 test_*.py 文件 收集测试函数/类 返回测试用例列表 准备fixtures 解析依赖关系 执行fixture函数 返回fixture值 执行测试用例 触发插件钩子 (pytest_runtest_call) 运行测试代码 执行断言 测试通过 测试失败 + 异常信息 测试跳过 记录测试结果 清理fixtures 生成测试报告 创建HTML/XML报告 统计测试结果 触发结束钩子 (pytest_sessionfinish) 插件清理工作
九、最佳实践总结
1. 项目结构
text
project/
├── pytest.ini
├── conftest.py
├── requirements.txt
├── src/ # 源代码
├── tests/ # 测试代码
│ ├── conftest.py
│ ├── unit/ # 单元测试
│ ├── integration/ # 集成测试
│ ├── api/ # API测试
│ │ ├── conftest.py
│ │ └── test_*.py
│ └── fixtures/ # 测试数据
│ └── *.yaml
├── reports/ # 测试报告
├── config/ # 配置文件
└── run_tests.py # 测试启动脚本
2. 常用命令
bash
# 基本运行
pytest tests/ -v
pytest tests/test_api.py::TestLogin
# 标记运行
pytest -m "smoke and not slow"
pytest -m "api or integration"
# 并行运行
pytest -n auto
# 失败重试
pytest --reruns 3 --reruns-delay 2
# 覆盖率
pytest --cov=src --cov-report=html
# 调试模式
pytest --pdb
3. 实用技巧
python
# 1. 跳过测试
@pytest.mark.skip(reason="功能未实现")
def test_unimplemented():
pass
# 2. 条件跳过
@pytest.mark.skipif(
sys.platform != "linux",
reason="仅在Linux运行"
)
# 3. 预期失败
@pytest.mark.xfail(reason="已知bug")
def test_buggy_feature():
assert False
# 4. 临时目录
def test_with_tempdir(tmpdir):
temp_file = tmpdir.join("test.txt")
temp_file.write("content")
assert temp_file.read() == "content"
# 5. 测试日志
def test_with_logging(caplog):
logging.getLogger().info("测试日志")
assert "测试日志" in caplog.text