—
前言
大家好,我是阿泽debug。做Python自动化测试5年了,Pytest是我的主力工具。很多测试同行用Pytest,但只用了基础功能(pytest.main()、@pytest.fixture)。今天这篇文章,我想分享Pytest的高级用法,让你的测试更强大、更优雅。
这篇文章覆盖:
- 参数化进阶
- 钩子函数(Hooks)
- 自定义插件开发
- 测试报告美化
- 性能优化技巧
—
一、参数化进阶
基础参数化
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框架会更强大、更专业。
—
参考资料
- Pytest官方文档:https://docs.pytest.org/
- Allure官方文档:https://docs.qameta.io/allure/
- pytest-xdist:https://pytest-xdist.readthedocs.io/
- RunnerGo博客系列
—
下期预告:Postman高级技巧揭秘 – 从接口测试到自动化Mock服务搭建