Pytest高级用法全攻略 – 参数化、钩子函数与插件开发实战

前言

大家好,我是阿泽debug。做Python自动化测试5年了,Pytest是我的主力工具。很多测试同行用Pytest,但只用了基础功能(pytest.main()@pytest.fixture)。今天这篇文章,我想分享Pytest的高级用法,让你的测试更强大、更优雅。

这篇文章覆盖:

一、参数化进阶

基础参数化

python
import pytest

单参数

@pytest.mark.parametrize(“input”, [1, 2, 3])
def test_single_param(input):
assert input

0

多参数

@pytest.mark.parametrize(“a, b, expected”, [
(1, 2, 3),
(10, 20, 30),
(-5, 5, 0),
])
def test_multi_params(a, b, expected):
assert a + b == expected

参数化 + ID命名

python
@pytest.mark.parametrize(“input, expected”, [
pytest.param(1, True, id=”正数”),
pytest.param(-1, False, id=”负数”),
pytest.param(0, False, id=”零”),
])
def test_with_ids(input, expected):
assert (input

0) == expected

运行时测试名称更清晰:

test_with_ids[正数] PASSED
test_with_ids[负数] PASSED
test_with_ids[零] PASSED

从文件读取参数

python
import yaml
import pytest

读取YAML文件作为测试数据

def load_test_data():
with open(“test_data.yaml”, “r”) as f:
return yaml.safe_load(f)

@pytest.mark.parametrize(“data”, load_test_data())
def test_from_yaml(data):
assert data[“input”] + data[“add”] == data[“expected”]

test_data.yaml内容:

– input: 1

add: 2

expected: 3

– input: 10

add: 5

expected: 15

参数化组合

python

多个parametrize会组合生成多个测试

@pytest.mark.parametrize(“x”, [1, 2])
@pytest.mark.parametrize(“y”, [10, 20])
def test_combination(x, y):
# 会生成4个测试:
# x=1, y=10
# x=1, y=20
# x=2, y=10
# x=2, y=20
assert x * y

0

条件跳过参数

python
import sys

@pytest.mark.parametrize(“value”, [
pytest.param(“windows_only”, marks=pytest.mark.skipif(sys.platform != “win32″, reason=”仅Windows”)),
pytest.param(“linux_only”, marks=pytest.mark.skipif(sys.platform != “linux”, reason=”仅Linux”)),
pytest.param(“all_platforms”, marks=pytest.mark.noop),
])
def test_platform_specific(value):
assert True

二、Fixture进阶

Fixture作用域

python
import pytest

function级别:每个测试函数调用一次

@pytest.fixture(scope=”function”)
def func_fixture():
return “function”

class级别:每个测试类调用一次

@pytest.fixture(scope=”class”)
def class_fixture():
return “class”

module级别:每个模块调用一次

@pytest.fixture(scope=”module”)
def module_fixture():
return “module”

session级别:整个测试会话调用一次

@pytest.fixture(scope=”session”)
def session_fixture():
return “session”

package级别:每个包调用一次

@pytest.fixture(scope=”package”)
def package_fixture():
return “package”

Fixture依赖

python

Fixture可以依赖其他Fixture

@pytest.fixture
def db_connection():
conn = connect_to_db()
yield conn
conn.close()

@pytest.fixture
def user_data(db_connection):
# 依赖db_connection
return db_connection.query(“SELECT * FROM users”)

def test_query(user_data):
# 自动获取user_data,间接获取db_connection
assert len(user_data)

0

Fixture自动执行

python

autouse=True:自动应用,不需要显式引用

@pytest.fixture(autouse=True)
def setup_teardown():
print(“测试前准备”)
yield
print(“测试后清理”)

def test_example():
# 不需要传入setup_teardown,自动执行
assert True

Fixture参数化

python
@pytest.fixture(params=[“chrome”, “firefox”, “edge”])
def browser(request):
# request.param获取当前参数值
driver = get_driver(request.param)
yield driver
driver.quit()

def test_with_browser(browser):
browser.get(“https://example.com”)
assert browser.title

conftest.py共享Fixture

python

tests/conftest.py

import pytest
import requests

@pytest.fixture(scope=”session”)
def api_client():
“””API测试客户端”””
session = requests.Session()
session.headers.update({“Authorization”: “Bearer test-token”})
return session

@pytest.fixture(scope=”session”)
def test_config():
“””测试配置”””
return {
“base_url”: “https://api.example.com”,
“timeout”: 30
}

所有测试文件都可以使用这些Fixture:
python

tests/api/test_users.py

def test_get_users(api_client, test_config):
resp = api_client.get(f”{test_config[‘base_url’]}/users”)
assert resp.status_code == 200

三、钩子函数(Hooks)

Pytest提供了丰富的钩子函数,可以自定义测试流程。

常用钩子函数

python

conftest.py

import pytest

测试开始前

def pytest_configure(config):
print(“Pytest配置阶段”)
config.addinivalue_line(“markers”, “slow:标记慢测试”)

测试会话开始

def pytest_sessionstart(session):
print(“测试会话开始”)

测试会话结束

def pytest_sessionfinish(session, exitstatus):
print(f”测试会话结束,状态:{exitstatus}”)

收集测试前

def pytest_collection_modifyitems(config, items):
# 可以修改测试列表
for item in items:
if “slow” in item.keywords:
item.add_marker(pytest.mark.skip(reason=”跳过慢测试”))

每个测试执行前

def pytest_runtest_setup(item):
print(f”准备执行:{item.name}”)

每个测试执行后

def pytest_runtest_teardown(item, nextitem):
print(f”清理测试:{item.name}”)

测试失败时

def pytest_runtest_logreport(report):
if report.failed:
print(f”测试失败:{report.nodeid}”)
# 可以在这里做失败截图、日志记录等

自定义测试报告

python
def pytest_runtest_makereport(item, call):
“””生成测试报告”””
if call.when == “call”:
# 测试执行阶段
if call.excinfo is not None:
# 测试失败
item.session.failed_tests.append(item.nodeid)
else:
# 测试成功
item.session.passed_tests.append(item.nodeid)

存储测试结果

@pytest.fixture(scope=”session”, autouse=True)
def store_results(request):
request.session.failed_tests = []
request.session.passed_tests = []

yield

# 会话结束后统计
print(f”n成功:{len(request.session.passed_tests)}”)
print(f”失败:{len(request.session.failed_tests)}”)

四、自定义插件开发

创建插件文件

python

pytest_custom_plugin.py

import pytest

class CustomPlugin:
“””自定义Pytest插件”””

def pytest_configure(self, config):
“””配置阶段”””
self.results = {“passed”: 0, “failed”: 0, “skipped”: 0}

def pytest_runtest_logreport(self, report):
“””记录测试结果”””
if report.when == “call”:
if report.passed:
self.results[“passed”] += 1
elif report.failed:
self.results[“failed”] += 1
else:
self.results[“skipped”] += 1

def pytest_sessionfinish(self, session, exitstatus):
“””输出自定义报告”””
print(“n” + “=”*50)
print(“自定义测试报告”)
print(“=”*50)
print(f”通过:{self.results[‘passed’]}”)
print(f”失败:{self.results[‘failed’]}”)
print(f”跳过:{self.results[‘skipped’]}”)
print(“=”*50)

注册插件

@pytest.fixture
def custom_plugin(request):
return CustomPlugin()

使用插件

bash

方式1:命令行指定

pytest -p pytest_custom_plugin

方式2:配置文件

pytest.ini

[pytest]
addopts = -p pytest_custom_plugin

五、Allure报告集成

安装配置

bash
pip install allure-pytest

基础用法

python
import pytest
import allure

@allure.feature(“用户管理”)
@allure.story(“登录功能”)
@allure.title(“验证用户登录成功”)
def test_login_success():
“””测试登录成功”””
with allure.step(“打开登录页面”):
# 实际操作
pass

with allure.step(“输入用户名密码”):
allure.attach(“用户名”, “testuser”)
allure.attach(“密码”, “password123”)

with allure.step(“点击登录按钮”):
pass

with allure.step(“验证登录成功”):
assert True

@allure.feature(“用户管理”)
@allure.story(“登录功能”)
@allure.severity(allure.severity_level.CRITICAL)
def test_login_failure():
“””测试登录失败”””
allure.attach.file(“screenshot.png”, name=”失败截图”, attachment_type=allure.attachment_type.PNG)
assert False

生成报告

bash

运行测试,生成报告数据

pytest –alluredir=./allure-results

启动报告服务

allure serve ./allure-results

生成静态报告

allure generate ./allure-results -o ./allure-report

六、性能优化技巧

并行测试

bash
pip install pytest-xdist

使用多进程并行

pytest -n auto # 自动检测CPU核数
pytest -n 4 # 使用4个进程

只运行失败的测试

bash

第一次运行(有失败)

pytest

只重新运行失败的测试

pytest –lf

先运行失败的,再运行其他的

pytest –ff

快速失败

bash

遇到第一个失败就停止

pytest -x

N次失败后停止

pytest –maxfail=3

测试分组

python

按标记分组测试

@pytest.mark.slow
def test_slow_1():
pass

@pytest.mark.fast
def test_fast_1():
pass

运行特定标记的测试

pytest -m slow # 只运行slow标记的测试
pytest -m “not slow” # 运行除slow外的测试
pytest -m “slow and fast” # 同时有这两个标记的测试

七、实战:完整测试框架示例

项目结构


tests/
├── conftest.py # 共享Fixture和钩子
├── test_data/ # 测试数据
│ ├── users.yaml
│ └── products.yaml
├── api/ # API测试
│ ├── test_users.py
│ ├── test_products.py
├── web/ # Web测试
│ ├── test_login.py
│ └── test_dashboard.py
└── utils/ # 工具函数
├── api_client.py
├── db_helper.py

conftest.py完整示例

python
import pytest
import yaml
import requests
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager

def load_yaml_data(file_path):
“””加载YAML测试数据”””
with open(file_path, “r”, encoding=”utf-8″) as f:
return yaml.safe_load(f)

============ Session级别Fixture ============

@pytest.fixture(scope=”session”)
def test_config():
“””全局测试配置”””
return {
“api_base_url”: “https://api.test.com”,
“web_base_url”: “https://web.test.com”,
“timeout”: 30,
“retry”: 3
}

@pytest.fixture(scope=”session”)
def api_session():
“””API请求Session”””
session = requests.Session()
session.headers.update({“Content-Type”: “application/json”})
yield session
session.close()

============ Module级别Fixture ============

@pytest.fixture(scope=”module”)
def user_data():
“””用户测试数据”””
return load_yaml_data(“test_data/users.yaml”)

============ Function级别Fixture ============

@pytest.fixture
def browser():
“””Web浏览器Driver”””
options = webdriver.ChromeOptions()
options.add_argument(“–headless”)
driver = webdriver.Chrome(ChromeDriverManager().install(), options=options)
driver.maximize_window()
yield driver
driver.quit()

@pytest.fixture
def auth_token(api_session, test_config):
“””获取认证Token”””
resp = api_session.post(
f”{test_config[‘api_base_url’]}/auth/login”,
json={“username”: “test”, “password”: “test”}
)
return resp.json()[“token”]

============ 钩子函数 ============

def pytest_configure(config):
“””配置阶段”””
config.addinivalue_line(“markers”, “api: API测试”)
config.addinivalue_line(“markers”, “web: Web测试”)
config.addinivalue_line(“markers”, “slow: 慢测试,默认跳过”)

def pytest_collection_modifyitems(config, items):
“””修改测试列表”””
# 添加测试名称的中文ID支持
for item in items:
item._nodeid = item._nodeid.encode(“utf-8”).decode(“unicode_escape”)

测试文件示例

python
import pytest
import allure

@allure.feature(“用户模块”)
class TestUsers:

@pytest.mark.api
@allure.story(“获取用户列表”)
def test_get_users(self, api_session, test_config, auth_token):
“””测试获取用户列表”””
headers = {“Authorization”: f”Bearer {auth_token}”}
resp = api_session.get(
f”{test_config[‘api_base_url’]}/users”,
headers=headers
)
assert resp.status_code == 200
assert len(resp.json())

0

@pytest.mark.api
@pytest.mark.parametrize(“user_id”, [1, 2, 3])
@allure.story(“获取单个用户”)
def test_get_user(self, api_session, test_config, auth_token, user_id):
“””测试获取单个用户”””
headers = {“Authorization”: f”Bearer {auth_token}”}
resp = api_session.get(
f”{test_config[‘api_base_url’]}/users/{user_id}”,
headers=headers
)
assert resp.status_code == 200
assert “id” in resp.json()

@pytest.mark.web
@allure.story(“用户登录页面”)
def test_login_page(self, browser, test_config):
“””测试登录页面”””
browser.get(f”{test_config[‘web_base_url’]}/login”)
assert “登录” in browser.title

username = browser.find_element(“id”, “username”)
password = browser.find_element(“id”, “password”)

username.send_keys(“testuser”)
password.send_keys(“password”)
browser.find_element(“id”, “submit”).click()

assert “dashboard” in browser.current_url

八、Pytest配置文件

ini

pytest.ini

[pytest]

测试目录

testpaths = tests

测试文件命名模式

python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*

命令行选项

addopts =
-v
–tb=short
–strict-markers
–alluredir=./allure-results

标记注册

markers =
api: API接口测试
web: Web页面测试
slow: 慢测试
smoke: 冒烟测试

日志配置

log_cli = true
log_cli_level = INFO
log_file = pytest.log
log_file_level = DEBUG

总结

Pytest高级功能总结:

功能 用途
参数化 减少重复代码,覆盖更多场景
Fixture作用域 控制资源创建时机,提高效率
钩子函数 自定义测试流程,扩展功能
插件开发 深度定制,满足特定需求
Allure报告 美化测试报告,可视化展示
pytest-xdist 并行测试,提升速度

掌握这些高级用法,你的Pytest框架会更强大、更专业。

参考资料

下期预告:Postman高级技巧揭秘 – 从接口测试到自动化Mock服务搭建

分享到:

探索 RunnerGo 全栈测试平台

RunnerGo 是一款面向企业的全栈测试平台,集接口测试、自动化测试、性能测试、UI测试于一体,助力企业提升研发效能。

接口测试
性能测试
自动化测试
免费体验 RunnerGo