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测试于一体,助力企业提升研发效能。

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