1. 项目概述为什么是pytest如果你正在做Python自动化测试或者打算从unittest、nose这些框架切换过来那么pytest绝对是你绕不开的一个选择。我最早接触它是因为团队里一个老鸟的推荐当时还在用unittest写着一堆self.assertEqual感觉代码又长又啰嗦。他丢过来一个用pytest写的测试文件我一看没有类没有继承断言直接用assert瞬间感觉清爽了。这不仅仅是语法糖它背后是一套更符合Python哲学、更强大的测试生态。简单说pytest是一个使编写小型测试变得简单同时又能支持复杂功能测试的框架。它的核心吸引力在于“约定大于配置”。你不用写一堆样板代码它就能自动发现并运行你的测试。这几年随着Python在自动化测试、数据科学、后端开发等领域的全面开花pytest几乎成了Python测试的事实标准。无论是做Web UI自动化配合Selenium/Playwright、接口自动化配合requests、还是单元测试pytest都能提供一套统一、高效的工具链。更关键的是它的插件生态极其丰富你可以像搭积木一样组合出适合自己项目的测试方案。所以这个“通关指南”的目标就是帮你从“知道pytest”到“能用pytest高效解决实际问题”让自动化测试真正成为你开发流程中的助力而不是负担。2. 核心设计哲学与快速上手2.1 理解pytest的“魔法”很多新手觉得pytest有些“魔法”比如为什么函数名以test_开头它就能自动运行为什么assert后面跟个表达式就能判断测试成败这其实都源于其精妙的设计。首先测试发现规则。pytest默认会递归查找当前目录及其子目录下所有文件名匹配test_*.py或*_test.py的文件。在这些文件中它会收集所有以test_开头的函数以及以Test开头的类中以test_开头的方法。你不需要显式地注册或导入它们pytest通过内省introspection自动完成。这大大减少了配置成本。其次断言重写。这是pytest的一大杀手锏。在Python中原生的assert语句在断言失败时只提供一个简单的AssertionError信息量很少。pytest在导入测试模块时会巧妙地重写rewrite字节码拦截assert语句。当断言失败时它能展示出更丰富的上下文信息比如表达式中各个变量的值。例如assert user.name “Admin”如果失败pytest会告诉你user.name实际是”Guest”而不是一个干巴巴的AssertionError。最后Fixture系统。这是pytest的灵魂我们后面会详细讲。你可以把它理解为测试的“脚手架”或“依赖注入”系统。通过pytest.fixture装饰器你可以定义一些可重用的准备和清理代码如创建数据库连接、初始化浏览器、准备测试数据然后在测试函数中通过参数声明来使用它。这解决了测试中常见的setup/teardown代码重复和依赖管理问题。2.2 5分钟搭建你的第一个pytest项目理论说再多不如动手。我们抛开复杂的IDE如PyCharm、VSCode配置用最原始的命令行来感受一下pytest的便捷。第一步环境准备。确保你安装了Python3.7及以上版本推荐。打开终端Windows用CMD或PowerShellMac/Linux用Terminal。第二步安装pytest。这是最简单的一步。pip install pytest为了验证安装可以运行pytest --version这会显示pytest的版本号。第三步编写第一个测试。在你喜欢的任何位置新建一个文件夹比如my_pytest_project。进入该文件夹创建一个名为test_sample.py的文件。用任何文本编辑器记事本、VSCode、Sublime都行打开它输入以下内容# test_sample.py def test_addition(): assert 1 1 2 def test_string_concatenation(): result Hello, pytest! assert result Hello, pytest! assert len(result) 5 class TestClassDemo: def test_one(self): x this assert h in x def test_two(self): x hello assert hasattr(x, upper)注意观察我们有两个独立的测试函数和一个包含两个测试方法的测试类。完全符合pytest的发现规则。第四步运行测试。在my_pytest_project文件夹下打开终端直接输入pytest你会看到类似这样的输出 test session starts platform win32 -- Python 3.9.0, pytest-7.4.0, pluggy-1.2.0 rootdir: C:\...\my_pytest_project collected 4 items test_sample.py .... [100%] 4 passed in 0.12s 太棒了pytest自动发现了4个测试并且全部通过用.表示。整个过程我们没有写任何运行测试的脚本比如unittest里的if __name__ “__main__”: unittest.main()也没有进行任何配置。这就是“约定大于配置”的魅力。注意如果你看到测试被收集但未运行或者提示“no tests ran”请首先检查1. 文件名是否以test_开头或以_test.py结尾2. 函数/方法名是否以test_开头3. 是否在正确的目录下执行了pytest命令这是新手最常见的三个坑。3. 核心功能深度解析与实战应用3.1 Fixture测试的基石与依赖管理Fixture是pytest最强大、最核心的概念。它用于为测试提供固定的、可预测的初始状态。想象一下每个测试用例可能都需要一个干净的数据库、一个登录后的用户会话、或者一个启动的浏览器。如果每个测试函数都自己写一遍这些代码那将是灾难性的重复和维护噩梦。定义一个简单的Fixture# conftest.py import pytest pytest.fixture def database_connection(): # 模拟建立数据库连接 print(\n建立数据库连接...) connection {connected: True, db: test_db} yield connection # 这是关键yield之前是setup之后是teardown # 模拟关闭连接 print(关闭数据库连接...) connection[connected] False我们把这段代码放在一个名为conftest.py的文件里。这个文件很特殊pytest会自动发现它并且其中定义的fixture可以被同一目录及子目录下的所有测试文件使用无需导入。在测试中使用Fixture# test_fixture_demo.py def test_query_user(database_connection): # 测试函数通过参数名直接“请求”fixture assert database_connection[connected] is True # 模拟查询操作 print(f在数据库 {database_connection[db]} 中查询用户...) assert True def test_insert_data(database_connection): assert database_connection[connected] is True print(f向数据库 {database_connection[db]} 插入数据...) assert True运行pytest -s test_fixture_demo.py-s参数允许打印输出你会看到每次测试执行前后连接建立和关闭的打印信息。yield是关键它让fixture变成了一个生成器yield之前的代码是“设置”setupyield返回的值这里是connection字典注入给测试函数测试函数执行完毕后会回到fixture中执行yield之后的代码进行“清理”teardown。这比传统的setup/teardown方法更清晰、更灵活。Fixture的作用域scope这是优化测试速度的关键。默认作用域是function即每个测试函数运行一次。但在某些场景下这是巨大的浪费。比如启动浏览器每次测试都重启浏览器会慢得无法忍受。pytest.fixture(scopemodule) def browser(): # 模拟启动一个重量级浏览器 print(\n 启动浏览器module级别只执行一次) driver {type: chrome, session_id: abc123} yield driver print( 关闭浏览器 ) pytest.fixture(scopesession) def login_user(): # 模拟用户登录整个测试会话session只登录一次 print(\n*** 用户登录session级别 ***) user {name: test_user, token: xyz789} return user # 也可以用return如果没有清理工作的话scope”function”默认每个测试函数都重新初始化。scope”class”每个测试类执行一次。scope”module”每个.py文件执行一次。scope”session”一次pytest命令执行过程即一个测试会话只执行一次。合理使用scope”module”或scope”session”可以极大提升测试套件的整体运行速度尤其是在UI自动化或需要复杂初始化的接口测试中。Fixture的自动使用autouse有些fixture你需要隐式地用到每个测试中比如清理临时文件夹、打日志。这时可以用autouseTrue。pytest.fixture(autouseTrue, scopefunction) def log_test_start_end(): print(f\n--- 开始测试 ---) yield print(f--- 结束测试 ---)这个fixture会自动应用于它作用域内的每一个测试无需在测试函数参数中声明。实操心得Fixture的命名要有意义不要用fixture1、data这种模糊的名字。好的命名如mock_database、chrome_browser、admin_user_session一看就知道用途。另外复杂的fixture逻辑可以拆分成多个小fixture然后通过fixture之间的依赖来组合。例如一个user_with_permissionfixture可以依赖于base_userfixture和permissionfixture。这让代码更清晰、更可复用。3.2 参数化测试告别重复代码当你需要对同一个功能用多组不同的输入数据进行测试时参数化Parametrization是你的最佳伙伴。它允许你定义一个测试函数然后让pytest用不同的参数多次运行它。基本用法import pytest # 测试一个简单的字符串反转函数假设有reverse_string函数 def reverse_string(s): return s[::-1] pytest.mark.parametrize(input_str, expected, [ (hello, olleh), (, ), (a, a), (12345, 54321), (Hello World, dlroW olleH), ]) def test_reverse_string(input_str, expected): result reverse_string(input_str) assert result expected, f反向‘{input_str}’得到‘{result}’但期望是‘{expected}’pytest.mark.parametrize装饰器第一个参数是一个字符串定义了注入测试函数的参数名这里是”input_str, expected”第二个参数是一个列表里面是元组每个元组对应一组参数值。pytest会运行这个测试函数5次每次注入不同的(input_str, expected)。参数化与Fixture结合这是更强大的模式。比如你想用不同的用户角色去测试同一个API端点。import pytest class User: def __init__(self, role): self.role role pytest.fixture(params[admin, editor, viewer]) def user_with_role(request): # request是一个内置fixture可以访问当前参数 return User(rolerequest.param) def test_api_access(user_with_role): # 这个测试会运行三次每次user_with_role是不同的User对象 if user_with_role.role admin: assert can_access_admin_panel(user_with_role) is True elif user_with_role.role editor: assert can_access_editor_tools(user_with_role) is True else: assert can_access_view_only(user_with_role) is True这里fixture通过params参数实现了参数化。测试函数test_api_access会运行三次每次接收到一个不同角色的User对象。注意事项参数化虽然强大但要避免过度使用。如果参数组合爆炸比如10个参数每个参数有5个值那就是10^5次运行会导致测试套件运行时间极长。此时应考虑1. 使用属性基测试Property-based testing工具如hypothesis。2. 精心挑选边界值、典型值和错误值而不是穷举所有可能。3. 将长时间运行的参数化测试标记为pytest.mark.slow并用-m “not slow”在快速反馈时跳过它们。3.3 标记Marking与选择性运行当你的测试套件有成百上千个用例时你肯定不想每次都全部运行。pytest的标记系统允许你对测试进行分类然后有选择地运行。内置标记pytest.mark.skip(reason“...” )无条件跳过某个测试。pytest.mark.skipif(condition, reason“...” )如果条件为真则跳过。pytest.mark.xfail(condition, reason“...” , runTrue, strictFalse)预期测试会失败。如果它失败了测试结果被记为XFAIL预期失败如果它通过了则记为XPASS意外通过。strictTrue时XPASS会被视为测试失败这有助于监控那些本应失败但被修复了的测试。自定义标记这是更常用的功能。你可以在pytest.ini配置文件中声明自定义标记以避免拼写错误警告。# pytest.ini [pytest] markers slow: marks tests as slow (deselect with ‘-m “not slow”‘) integration: integration tests that require external services smoke: subset of tests for quick verification然后在测试中使用它们import pytest import time pytest.mark.slow def test_complex_calculation(): time.sleep(5) # 模拟一个耗时操作 assert some_heavy_computation() expected_result pytest.mark.integration def test_api_with_real_backend(): # 这个测试需要连接真实的、可能不稳定的第三方API response call_real_api() assert response.status_code 200 pytest.mark.smoke def test_login_functionality(): # 冒烟测试核心功能 assert login(valid_user, valid_pass) is True如何运行只运行冒烟测试pytest -m smoke运行除集成测试外的所有测试pytest -m “not integration”同时满足多个标记AND逻辑pytest -m “slow and smoke”很少用满足任一标记OR逻辑pytest -m “slow or integration”通过关键字过滤除了标记还可以用-k选项通过测试名中的子字符串来过滤。pytest -k “login” # 运行所有名称中包含“login”的测试 pytest -k “not slow” -k “api” # 运行名称含“api”但不含“slow”的测试-m和-k的结合使用让你能极其灵活地控制测试范围这在持续集成CI pipeline中设置不同的测试阶段如快速测试、完整测试、夜间构建测试时非常有用。3.4 插件生态扩展你的测试能力pytest本身是一个核心其强大之处在于丰富的插件生态。安装插件就像pip install一样简单。几个必知必会的核心插件pytest-html生成漂亮的HTML测试报告。pip install pytest-html pytest --htmlreport.html这会在当前目录生成一个report.html文件用浏览器打开可以看到清晰的测试结果汇总、通过/失败详情甚至控制台输出。这对于向非技术同事如项目经理展示测试结果非常友好。pytest-xdist实现测试的分布式并行执行大幅加速测试。pip install pytest-xdist pytest -n auto # 使用与CPU核心数相同的worker并行运行 pytest -n 4 # 使用4个worker并行运行对于大型测试套件这是提升反馈速度的神器。但要注意测试必须是线程安全的不能有共享状态冲突。通常涉及外部资源如数据库、文件的测试需要小心处理。pytest-cov生成测试覆盖率报告。pip install pytest-cov pytest --covmy_project # 计算my_project包的覆盖率 pytest --covmy_project --cov-reporthtml # 生成HTML格式的覆盖率报告覆盖率报告能直观地告诉你哪些代码被测试覆盖了哪些是“盲区”。它是衡量测试完备性的重要但不是唯一指标。pytest-mock一个对unittest.mock的包装让模拟Mocking更符合pytest风格。虽然Python标准库的unittest.mock已经很强但pytest-mock提供了一个mockerfixture用起来更方便。def test_payment(mocker): # mocker是pytest-mock提供的fixture mock_charge mocker.patch(‘payment_gateway.charge_credit_card‘) mock_charge.return_value {“success”: True, “transaction_id”: “txn_123”} result process_payment(user_id1, amount100) mock_charge.assert_called_once_with(user_id1, amount100) assert result is Truepytest-asyncio用于测试异步代码asyncio。import pytest import asyncio pytest.mark.asyncio async def test_async_fetch(): result await fetch_data(“http://example.com“) assert result “expected data”如何寻找和管理插件官方插件列表在 pytest.org 你也可以在PyPI上搜索pytest-*。对于大型项目建议在requirements-test.txt或pyproject.toml中固定测试依赖的版本以保证测试环境的稳定性。4. 构建企业级自动化测试框架掌握了核心概念后我们需要把它们组合起来搭建一个结构清晰、易于维护的自动化测试框架。这里以接口自动化测试为例因为它兼具复杂性和实用性。4.1 项目结构设计一个良好的目录结构是维护性的基础。我推荐如下结构my_api_tests/ ├── conftest.py # 全局fixture和钩子函数 ├── pytest.ini # 项目配置文件 ├── requirements.txt # 项目依赖或使用pyproject.toml ├── common/ # 公共模块 │ ├── __init__.py │ ├── logger.py # 日志配置 │ ├── config.py # 读取配置文件环境、URL等 │ └── exceptions.py # 自定义异常 ├── core/ # 核心业务封装 │ ├── __init__.py │ └── api_client.py # 封装的HTTP请求客户端 ├── test_data/ # 测试数据JSON, YAML等 │ └── users.yaml ├── test_cases/ # 测试用例目录按模块组织 │ ├── __init__.py │ ├── test_user_auth.py │ └── test_product.py └── reports/ # 测试报告输出目录.gitignore忽略 └── html/关键文件解析conftest.py这是pytest的“魔法”文件。你可以在这里定义被所有测试模块共享的fixture。例如全局的请求会话、数据库连接池、日志初始化等。pytest.ini统一配置pytest行为。例如设置默认命令行参数、注册自定义标记、配置测试路径等。[pytest] testpaths test_cases markers smoke: smoke tests regression: regression tests addopts -v --tbshort -laddopts定义了默认运行参数-v详细输出--tbshort使用简短的错误回溯-l显示局部变量值失败时很有用。core/api_client.py这是框架的核心。不要在每个测试用例里直接写requests.get()。封装一个客户端统一处理请求头、认证、日志、异常重试、响应解析等。# core/api_client.py import requests from common.logger import get_logger from common.config import settings log get_logger(__name__) class ApiClient: def __init__(self, base_urlNone): self.base_url base_url or settings.API_BASE_URL self.session requests.Session() self.session.headers.update({“Content-Type”: “application/json”}) # 可以在这里加载token等认证信息 def request(self, method, endpoint, **kwargs): url f”{self.base_url}{endpoint}” log.info(f”Request: {method} {url}“) resp self.session.request(method, url, **kwargs) log.info(f”Response Status: {resp.status_code}“) log.debug(f”Response Body: {resp.text}“) resp.raise_for_status() # 非2xx状态码抛出HTTPError return resp def get(self, endpoint, paramsNone): return self.request(“GET”, endpoint, paramsparams) def post(self, endpoint, json_dataNone): return self.request(“POST”, endpoint, jsonjson_data) # ... 其他HTTP方法4.2 数据驱动测试实战将测试数据与测试逻辑分离是提高用例可维护性的关键。我们可以使用YAML或JSON文件来管理测试数据。1. 准备测试数据文件 (test_data/users.yaml):login_cases: - name: “登录成功 - 普通用户” username: “test_user” password: “password123” expected: status_code: 200 has_token: true user_role: “user” - name: “登录成功 - 管理员” username: “admin” password: “admin123” expected: status_code: 200 has_token: true user_role: “admin” - name: “登录失败 - 密码错误” username: “test_user” password: “wrong” expected: status_code: 401 error_msg: “Invalid credentials” - name: “登录失败 - 用户不存在” username: “nonexistent” password: “any” expected: status_code: 404 error_msg: “User not found”2. 编写数据读取Fixture (conftest.py):# conftest.py import pytest import yaml import os def load_yaml_test_data(file_name): file_path os.path.join(os.path.dirname(__file__), ‘test_data’, file_name) with open(file_path, ‘r’, encoding‘utf-8’) as f: data yaml.safe_load(f) return data pytest.fixture(paramsload_yaml_test_data(‘users.yaml’)[‘login_cases’]) def login_test_case(request): # 这个fixture被参数化了参数来自YAML文件 return request.param3. 编写测试用例 (test_cases/test_user_auth.py):# test_cases/test_user_auth.py import pytest from core.api_client import ApiClient class TestUserAuthentication: pytest.fixture(scope“class”) def api_client(self): # 类级别的客户端这个测试类中的所有用例共享一个session return ApiClient() def test_login(self, api_client, login_test_case): “”“数据驱动的登录测试”“” case login_test_case payload { “username”: case[“username”], “password”: case[“password”] } # 发送请求 response api_client.post(“/api/v1/login”, json_datapayload) # 断言状态码 assert response.status_code case[“expected”][“status_code”], \ f”用例 ‘{case[“name”]}’ 状态码断言失败” resp_json response.json() # 根据预期动态断言 if case[“expected”][“status_code”] 200: assert “token” in resp_json, f”用例 ‘{case[“name”]}’ 响应中应包含token” assert resp_json.get(“user”, {}).get(“role”) case[“expected”][“user_role”] else: # 登录失败的情况 assert “error” in resp_json assert case[“expected”][“error_msg”] in resp_json[“error”]运行这个测试pytest会自动运行4次test_login每次注入YAML文件中的一组数据。测试报告里会清晰显示每个数据用例的执行结果。当登录接口的请求体或响应结构发生变化时你只需要修改YAML文件和少量的断言逻辑而不是翻遍几十个测试函数。4.3 测试报告与持续集成集成生成可视化的测试报告并与CI/CD工具如Jenkins, GitLab CI, GitHub Actions集成是自动化测试闭环的关键。1. 生成组合报告我们通常希望同时拥有控制台的详细输出和HTML的直观报告。# 运行测试并生成JUnit XML格式报告很多CI工具认这个格式和HTML报告 pytest -v --junitxmlreports/junit.xml --htmlreports/html/report.html --self-contained-html--junitxml生成JUnit格式的XML报告Jenkins等工具可以解析它来展示测试趋势和历史。--html生成HTML报告。--self-contained-html选项会将CSS样式内联生成单个HTML文件便于传输和查看。可以将这些命令写入项目的Makefile或scripts/test.sh中。2. 与GitHub Actions集成示例在项目根目录创建.github/workflows/test.yml。name: Run Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: ‘3.9’ - name: Install dependencies run: | pip install -r requirements.txt pip install pytest pytest-html pytest-xdist - name: Run tests with pytest run: | pytest -v --junitxmlreports/junit.xml --htmlreports/html/report.html --self-contained-html - name: Upload test report uses: actions/upload-artifactv3 if: always() # 即使测试失败也上传报告 with: name: pytest-report path: reports/这样每次代码推送或发起拉取请求时都会自动运行测试套件并将生成的报告保存为工件可供下载查看。5. 高级技巧与避坑指南5.1 测试固件Fixture的依赖与作用域陷阱Fixture可以依赖其他Fixture这带来了强大的组合能力但也容易引入作用域冲突。问题场景一个session级别的fixturedb_pool数据库连接池一个function级别的fixturetransaction数据库事务依赖于db_pool。你希望每个测试函数在一个独立的事务中运行测试结束后回滚。pytest.fixture(scope“session”) def db_pool(): pool create_db_pool() yield pool pool.close() pytest.fixture(scope“function”) def transaction(db_pool): # 依赖session级别的db_pool conn db_pool.getconn() trans conn.begin() yield conn trans.rollback() db_pool.putconn(conn) def test_create_user(transaction): # 使用transaction pass这看起来没问题。但陷阱在于如果db_pool是session级别那么在整个测试会话中所有transactionfixture拿到的都是同一个连接池对象。这通常是安全的。但如果你错误地将db_pool也设为function级别那么每个测试函数都会创建和关闭一个全新的连接池性能极差。最佳实践仔细规划fixture的作用域。资源昂贵的对象HTTP会话、数据库连接池、浏览器驱动尽量用session或module级别。对于需要隔离的、有状态的对象如数据库事务、临时文件使用function级别。在conftest.py中清晰地注释每个fixture的作用域和用途。使用pytest --setup-show test_file.py命令可以可视化查看fixture的创建和销毁过程帮助你理解作用域和依赖关系。5.2 Mock的精准使用与过度MockMock模拟是单元测试和集成测试中隔离外部依赖的利器但滥用Mock会让测试失去意义。常见误区过度Mock即把被测代码依赖的所有外部函数、类都Mock掉最后测试只是在验证自己写的Mock逻辑。# 不好的例子过度Mock def test_process_order(mocker): mock_db mocker.patch(‘module.get_database_connection’) mock_query mock_db.return_value.query mock_query.return_value [{“id”: 1}] mock_send_email mocker.patch(‘module.send_email’) mock_charge mocker.patch(‘module.charge_credit_card’) mock_charge.return_value True result process_order(1) assert result is True mock_send_email.assert_called_once()这个测试Mock了数据库、邮件服务和支付网关。它几乎没测试到任何真实业务逻辑只是检查了函数调用顺序。如果process_order内部的业务逻辑非常复杂这个测试覆盖不到。更好的策略分层测试对process_order这样复杂的服务层函数更适合做集成测试或端到端测试使用真实的测试数据库和模拟的第三方支付网关使用sandbox环境。使用Fake而非Mock对于一些存储可以创建一个内存中的“假”实现Fake Repository它拥有和真实存储一样的接口但数据存在内存里。这样测试既快速又能测试更多真实逻辑。明智地选择Mock边界Mock应该用于那些不稳定、速度慢或有副作用的外部服务如第三方API、邮件发送、短信网关、支付接口。对于项目内部的、稳定的模块尽量使用真实实现或Fake。5.3 测试失败的有效分析与调试当测试失败时pytest提供了丰富的信息来帮助你定位问题。1. 理解输出信息F测试失败Failure。断言未通过。E测试错误Error。测试代码本身抛出了异常如导入错误、fixture错误。s跳过Skipped。x预期失败XFAIL。X预期失败但通过了XPASS。2. 使用-v和--tb选项pytest -v输出详细信息包括每个测试的名字。pytest --tbshort只显示失败位置的简短回溯信息更聚焦。pytest --tbno不显示回溯只显示总结。pytest --lf或--last-failed只重新运行上一次失败的测试。这在调试时非常有用。3. 使用-l--showlocals选项当测试失败时打印出失败时刻测试函数内的所有局部变量及其值。这是调试神器很多时候看一眼变量值就知道问题所在。pytest -l4. 使用PDB进行交互式调试在怀疑的代码行前插入import pdb; pdb.set_trace()或者直接使用pytest --pdb选项当测试失败时自动进入pdb调试器。你可以检查变量、执行代码逐步排查。5. 分析HTML报告对于复杂的失败HTML报告比控制台输出更易读。它可以展开查看每个失败测试的完整错误信息、日志输出和截图如果集成了。5.4 性能优化让测试跑得更快测试套件变慢是大型项目的通病。以下是一些提速策略使用pytest-xdist并行运行如前所述这是最直接的提速手段。确保测试是独立的没有共享状态竞争。优化Fixture作用域将scope”function”的重量级fixture如启动浏览器提升为scope”class”或scope”module”让一个类或模块内的测试共享同一个实例。使用Mock或Fake替代慢速依赖如果测试依赖一个响应很慢的外部API果断Mock它。分离测试套件使用标记mark将测试分类。在开发阶段只运行快速的单元测试pytest -m “not slow and not integration”。在CI的合并请求检查中运行全部单元测试和部分关键的集成测试。只在夜间构建或发布前运行全部测试包括慢速的端到端测试。保持测试数据库小巧且快速使用内存数据库如SQLite:memory:进行单元测试。对于集成测试使用专门优化的测试数据库实例并定期清理旧数据。避免不必要的setUp/tearDown在每个测试中只准备它真正需要的数据而不是重置整个数据库。可以使用事务回滚pytest.fixture配合yield和rollback来保证测试隔离而不是全表删除。6. 从pytest到现代测试实践掌握了pytest你已经拥有了强大的武器。但要构建真正可靠的测试体系还需要一些现代测试理念的加持。1. 测试金字塔牢记测试金字塔模型——底层是大量快速、低成本的单元测试用pytest mock中间是少量集成测试用pytest 真实数据库顶层是极少量的端到端UI测试用pytest Selenium/Playwright。pytest可以贯穿整个金字塔。2. 属性基测试Property-based Testing除了用pytest.mark.parametrize手动设计测试用例还可以使用hypothesis库。它通过生成大量随机、边缘的输入数据来测试你的代码能发现你没想到的bug。from hypothesis import given, strategies as st given(st.integers(), st.integers()) def test_addition_commutative(a, b): assert add(a, b) add(b, a) # 测试加法交换律3. 快照测试Snapshot Testing对于输出结构复杂但相对稳定的函数如生成配置、渲染模板可以使用pytest-instafail或syrupy等插件进行快照测试。第一次运行时它会将输出保存为“快照”文件。后续运行会与快照对比如果不同则测试失败。这非常适合检测非预期的输出变化。4. 测试即文档好的测试用例本身就是最好的文档。测试函数名应该清晰地描述其行为如test_login_fails_with_invalid_password。使用pytest -v运行时这些名字就是一份可执行的规格说明。最后我个人最深的体会是自动化测试不是一蹴而就的。不要试图一开始就写出完美的、覆盖100%的测试。从为最核心、最脆弱的代码写测试开始让测试随着项目一起成长。将pytest集成到你的开发流程中每次修改代码后都运行相关的测试套件让它成为你代码信心的安全网。当测试失败时不要把它看作负担而是一个发现潜在问题、理解代码行为的宝贵机会。