FEATURED · 精选文章

Python测试框架pytest:从极简语法到高级Fixture的完整指南

发布时间 / 2026/8/13 5:53:19
来源 / 创域科博编辑部
栏目 / 资讯中心
Python测试框架pytest:从极简语法到高级Fixture的完整指南 1. 项目概述为什么是pytest如果你写过Python代码尤其是写过那么一丁点测试那你大概率听说过unittest、nose当然还有今天的主角——pytest。几年前我刚接触自动化测试时也是从unittest开始的但用着用着就发现写个测试用例还得继承一个TestCase类断言方法名字又长又怪assertEqualassertTrue想临时跳过某个测试还得用装饰器一套流程下来感觉不是在写测试而是在写某种“仪式感”很强的样板代码。后来团队里一个老鸟扔给我一个用pytest写的测试文件我一看嚯这不就是普通的Python函数吗用个assert关键字就搞定了运行命令也简单到就一个pytest。从那一刻起我就基本没怎么回头用过unittest了。所以这个“十分钟带你看懂”系列目的不是让你十分钟成为专家而是帮你用最短的时间看透pytest这个框架的核心设计哲学和它能为你带来的最大便利。它绝不仅仅是另一个测试运行器它是一套完整的、以“约定优于配置”和“极简主义”为灵魂的测试生态系统。它能让你用更符合Pythonic风格的方式写测试用更强大的功能组织测试并用更灵活的方式运行测试。无论是写几个函数单元测试还是构建一个成百上千用例的接口自动化项目pytest都能提供恰到好处的支持。这篇文章我就结合自己踩过的无数坑和总结的最佳实践带你从安装到高级玩法把pytest里里外外捋一遍。2. pytest的核心优势与设计哲学2.1 极简的语法告别样板代码pytest最吸引人的地方就是它的简洁。它不需要你继承任何特定的类。一个测试用例就是一个以test_开头的函数一个测试类就是一个以Test开头的类并且不能有__init__方法里面的测试方法同样以test_开头。断言直接用Python原生的assert语句。这让测试代码看起来和你的业务代码一样干净。举个例子我们测试一个简单的函数计算两个数的和# code_under_test.py def add(a, b): return a b用unittest的写法import unittest from code_under_test import add class TestAdd(unittest.TestCase): def test_add_integers(self): self.assertEqual(add(1, 2), 3) def test_add_floats(self): self.assertAlmostEqual(add(0.1, 0.2), 0.3)用pytest的写法from code_under_test import add def test_add_integers(): assert add(1, 2) 3 def test_add_floats(): # 注意浮点数比较的坑pytest可以用approx from pytest import approx assert add(0.1, 0.2) approx(0.3)高下立判。pytest的写法更直观更接近我们思考的逻辑“我调用add(1,2)然后断言它等于3”。当你需要断言一个复杂的对象或条件时原生的assert语句配合Python强大的表达式能力比记忆一堆assertXxx方法要自由得多。注意pytest会重写assert语句以提供更丰富的失败信息。这是它的魔法之一。当断言失败时它会清晰地展示表达式中左右两边的值甚至能对列表、字典等进行差异对比这比unittest简单的AssertionError信息量大多了。2.2 强大的Fixture测试资源的生命周期管理如果说简洁的语法是pytest的门面那么Fixture就是它的心脏和灵魂。这是pytest区别于其他框架最核心的特性。你可以把Fixture理解为测试的“脚手架”或“依赖注入”机制。它用于准备测试环境如数据库连接、临时文件、API客户端和清理工作。定义一个Fixture非常简单使用pytest.fixture装饰器。它的威力在于其作用域scope和自动注入能力。import pytest import tempfile import os pytest.fixture(scopefunction) # 默认作用域是function即每个测试函数运行一次 def temporary_file(): 创建一个临时文件并在测试结束后清理。 # 准备工作创建文件 temp tempfile.NamedTemporaryFile(modew, deleteFalse, suffix.txt) temp.write(Initial content) temp.close() file_path temp.name yield file_path # 将资源提供给测试用例 # 清理工作删除文件 os.unlink(file_path) def test_write_to_file(temporary_file): # 测试函数通过参数名自动请求这个fixture with open(temporary_file, a) as f: f.write(\nAppended content) with open(temporary_file, r) as f: content f.read() assert Appended content in content # 测试结束后会自动执行fixture中yield之后的清理代码删除文件。Fixture的作用域function默认每个测试函数运行一次。class每个测试类运行一次该类中的所有方法共享同一个fixture实例。module每个.py文件运行一次。package每个包目录运行一次。session整个测试会话一次pytest命令执行只运行一次。合理使用作用域能极大提升测试效率。例如数据库连接通常用session或module级别避免每个测试都重复建立连接而一个干净的数据库表可能用function级别确保测试间的隔离。实操心得不要滥用session级别的fixture。虽然它效率最高但如果fixture内部状态被某个测试修改了可能会污染后续测试导致测试间产生意想不到的依赖这是自动化测试的大忌。对于有状态、需要隔离的资源优先考虑function级别。2.3 灵活的测试发现与执行pytest的发现规则非常直观递归查找当前目录及子目录下所有以test_开头或_test结尾的.py文件然后在这些文件中查找以test_开头的函数或以Test开头的类其内部以test_开头的方法。执行起来更是灵活运行所有测试pytest运行指定文件pytest test_module.py运行指定目录pytest tests/运行匹配名称的测试pytest -k “add or multiply”运行名称中包含add或multiply的测试运行指定标记的测试pytest -m slow运行被pytest.mark.slow装饰的测试从上次失败的地方运行pytest --lf并行运行测试需要安装pytest-xdistpytest -n 4这种灵活性使得在大型项目中快速定位和运行特定子集测试变得轻而易举非常适合持续集成CI环境下的测试策略。3. 从零开始pytest环境搭建与基础使用3.1 安装与最小化验证安装pytest简单到只需一行命令。强烈建议在虚拟环境venv或conda中进行以隔离项目依赖。pip install pytest安装后验证一下。创建一个最简单的测试文件test_sample.py# test_sample.py def test_passing(): assert 1 1 2 def test_failing(): assert 2 * 2 5 # 这个会失败在命令行进入该文件所在目录运行pytestpytest test_sample.py -v # -v 参数表示详细输出你会看到类似这样的输出清晰地标明了通过和失败的测试以及失败断言的具体信息 test session starts platform darwin -- Python 3.9.0, pytest-7.0.0, pluggy-1.0.0 rootdir: /path/to/your/tests collected 2 items test_sample.py::test_passing PASSED [ 50%] test_sample.py::test_failing FAILED [100%] FAILURES ________________________________ test_failing _________________________________ def test_failing(): assert 2 * 2 5 # 这个会失败 E assert 4 5 test_sample.py:5: AssertionError short test summary info FAILED test_sample.py::test_failing - assert 4 5 1 failed, 1 passed in 0.12s 3.2 配置文件pytest.ini虽然pytest开箱即用但通过配置文件pytest.ini可以定制其行为。这个文件通常放在项目根目录。一个典型的pytest.ini示例[pytest] # 指定测试文件的搜索模式 python_files test_*.py *_test.py # 指定测试类和函数的命名模式 python_classes Test* python_functions test_* # 添加命令行默认选项例如总是显示详细结果和打印输出 addopts -v -s # 定义自定义标记及其说明防止拼写错误 markers slow: marks tests as slow (deselect with -m “not slow”‘) integration: marks tests as integration tests smoke: smoke test suite # 设置测试搜索的根目录 testpaths tests unit integration # 忽略某些目录 norecursedirs .* build dist *.egg-infoaddopts非常有用。比如你总是喜欢看打印输出-s和详细结果-v就可以在这里设置而不用每次都在命令行敲。注意事项pytest.ini的优先级很高。如果团队有统一的配置务必将其纳入版本控制。同时小心addopts中的设置可能会覆盖CI脚本中传入的参数。3.3 断言的艺术善用pytest的断言重写我们之前提到pytest重写了assert。它的强大之处在于对复杂数据结构的对比。看这个例子def test_complex_data(): expected { user: alice, age: 30, hobbies: [reading, hiking], address: {city: Shanghai, zip: 200000} } actual { user: alice, age: 31, # 这里不同 hobbies: [reading, swimming], # 这里也不同 address: {city: Shanghai, zip: 200000} } assert actual expected运行这个测试pytest会给出极其详细的差异对比直接告诉你哪个字段不匹配列表里哪个元素不一样就像git diff一样清晰。这是自己写字符串拼接的断言信息无法比拟的。对于浮点数比较务必使用pytest.approx避免因浮点精度问题导致的误报。from pytest import approx def test_float_calculation(): result 0.1 0.2 # 错误写法assert result 0.3 # 正确写法 assert result approx(0.3) # 还可以指定相对或绝对容差 assert 9.9999999 approx(10, rel1e-6) # 相对容差1e-64. Fixture的深入使用与最佳实践4.1 Fixture的依赖注入与参数化Fixture可以依赖其他Fixture形成清晰的依赖链。pytest会自动解析这些依赖并按正确的顺序执行。import pytest pytest.fixture def database_connection(): conn create_db_connection() yield conn conn.close() pytest.fixture def empty_user_table(database_connection): # 依赖database_connection db database_connection db.execute(“DROP TABLE IF EXISTS users”) db.execute(“CREATE TABLE users (id INT, name TEXT)”) yield db db.execute(“DROP TABLE users”) def test_insert_user(empty_user_table): db empty_user_table db.execute(“INSERT INTO users VALUES (1, ‘Alice’)”) # ... 进行断言更强大的是Fixture本身可以被参数化。这意味着你可以用一个Fixture定义为测试提供多组不同的数据。import pytest pytest.fixture(params[‘chrome’, ‘firefox’, ‘edge’]) def browser(request): # request是一个内置fixture用于访问参数 driver initialize_webdriver(request.param) yield driver driver.quit() def test_login(browser): # 这个测试会运行三次每次使用不同的browser browser.get(“http://example.com/login”) # ... 执行登录操作 assert browser.title “Dashboard”4.2 内置Fixture与常用插件pytest提供了一些非常有用的内置Fixturetmp_path/tmpdir提供临时目录路径pathlib.Path对象或py.path.local对象测试结束后自动清理。强烈推荐使用tmp_path它是标准的pathlib.Path对象。monkeypatch用于在测试中动态修改对象、函数、环境变量等测试后自动恢复。这是实现单元测试“打补丁”的利器。capsys/capfd捕获标准输出/标准错误用于测试打印语句或日志。request提供当前测试请求的上下文信息常用于参数化fixture中。def test_with_tmp(tmp_path): # tmp_path是一个Path对象 d tmp_path / “sub” d.mkdir() p d / “hello.txt” p.write_text(“content”) assert p.read_text() “content” # 测试结束临时目录/tmp/...会被自动删除 import os def test_env_var(monkeypatch): monkeypatch.setenv(“MY_SETTING”, “test_value”) assert os.environ[“MY_SETTING”] “test_value” # 测试结束后环境变量会被恢复原状 def test_output(capsys): print(“Hello, pytest!”) captured capsys.readouterr() assert captured.out “Hello, pytest!\n”此外pytest的插件生态极其繁荣。几个必知的插件pytest-xdist分布式/并行测试大幅缩短测试套件运行时间。pytest-cov集成覆盖率工具coverage.py生成测试覆盖率报告。pytest-mock集成了unittest.mock提供更简洁的mock语法其实pytest本身通过monkeypatch也能很好完成mock。pytest-html生成漂亮的HTML测试报告。pytest-asyncio用于测试异步asyncio代码。4.3 Fixture作用域选择的实战考量选择Fixture作用域是一个平衡艺术核心考量是隔离性与性能。数据库连接通常使用session作用域。建立数据库连接是昂贵的操作一个会话内所有测试共享一个连接池是合理的。但要注意如果测试会修改数据库全局状态如修改某个配置表则需要更细的隔离或者使用事务并在每个测试后回滚这通常通过function级别的fixture包装事务来实现。WebDriver实例对于Selenium UI测试启动浏览器非常慢。如果测试之间浏览器状态可以独立通过每次打开新标签页或清理cookies可以考虑class或module作用域共享一个浏览器实例。但UI测试本身不稳定一个测试的失败可能导致浏览器状态异常影响后续测试。因此更稳妥的做法是使用function作用域并通过pytest-xdist并行化来弥补启动开销。测试数据准备比如一个只读的参考数据文件可以用session作用域加载一次。如果是需要被修改的测试数据则必须用function作用域确保每个测试开始时数据都是干净的。一个常见的模式是使用嵌套Fixture来组合不同作用域的优势pytest.fixture(scope”session”) def db_engine(): 昂贵的数据库引擎整个会话一个。 engine create_engine(‘sqlite:///:memory:’) yield engine engine.dispose() pytest.fixture(scope”function”) # 每个测试函数一个独立事务 def db_session(db_engine): 为每个测试提供一个独立的事务会话测试后回滚。 connection db_engine.connect() transaction connection.begin() session Session(bindconnection) yield session session.close() transaction.rollback() # 回滚保证数据库状态干净 connection.close() def test_create_user(db_session): # 每个测试都获得一个干净的session user User(name‘test’) db_session.add(user) db_session.commit() assert user.id is not None5. 参数化测试与标记高效覆盖多种场景5.1 使用pytest.mark.parametrize当你想用多组数据测试同一个功能时逐一定义测试函数是低效的。pytest.mark.parametrize装饰器是解决这个问题的标准答案。import pytest def is_valid_email(email): # 一个简单的不严谨的邮箱验证函数 return ‘’ in email and ‘.’ in email.split(‘’)[-1] pytest.mark.parametrize(“email, expected”, [ (“aliceexample.com”, True), (“bobexample.co.uk”, True), (“invalid-email”, False), (“user”, False), (“domain.com”, False), (“”, False), (None, False), # 边界情况 ]) def test_email_validation(email, expected): assert is_valid_email(email) expected这个测试会运行7次每次使用一组(email, expected)数据。在输出中每组参数都会作为一个独立的测试用例显示非常清晰。参数化也支持更复杂的结构比如嵌套参数化或者从函数动态生成参数列表。import itertools def generate_login_data(): users [‘alice’, ‘bob’] passwords [‘pass123’, ‘secret!’] return list(itertools.product(users, passwords)) pytest.mark.parametrize(“username, password”, generate_login_data()) def test_login_with_params(username, password): # 这会生成2*24个测试用例 result login(username, password) assert result is not None5.2 自定义标记与测试分类标记Mark用于给测试分类以便选择性地运行。你可以用pytest.mark.smoke标记冒烟测试用pytest.mark.integration标记集成测试。首先需要在pytest.ini中声明这些标记避免拼写错误[pytest] markers smoke: quick tests for basic functionality integration: tests that involve external systems slow: tests that take a long time to run然后就可以在测试上使用import time pytest.mark.smoke def test_homepage_loads(): # 快速检查主页是否能访问 assert check_url(“http://example.com”) 200 pytest.mark.integration pytest.mark.slow def test_full_user_workflow(): # 一个耗时很长的端到端流程测试 time.sleep(10) # … 复杂的业务流程 assert workflow_result “success”运行命令只运行冒烟测试pytest -m smoke运行除慢速测试外的所有测试pytest -m “not slow”同时满足多个标记pytest -m “smoke and integration”较少用满足任一标记pytest -m “smoke or integration”实操心得标记是组织测试套件的强大工具。在CI/CD流水线中可以配置不同的任务每次提交都运行smoke测试每晚定时运行全部测试包括slow合并请求时运行integration测试。关键在于团队要对标记的含义达成一致并严格遵守。6. 插件生态与高级功能探索6.1 测试报告生成pytest-html与Allure清晰的测试报告对于团队协作和问题定位至关重要。pytest-html可以快速生成一个HTML报告。pip install pytest-html pytest --htmlreport.html --self-contained-html生成的report.html文件包含了测试概述、通过/失败/跳过的详细列表、以及每个失败测试的断言错误信息可以直接在浏览器中打开分享。对于更专业、更美观的报告Allure是一个行业标准的选择。它需要额外安装allure-pytest插件和allure命令行工具。pip install allure-pytest # 运行测试并生成Allure结果数据 pytest --alluredir./allure-results # 使用Allure命令行生成可交互的HTML报告需要先安装Allure命令行工具 allure serve ./allure-resultsAllure报告提供了仪表盘、趋势图、测试用例分组、丰富的附件截图、日志支持等功能非常适合大型项目。6.2 测试覆盖率分析pytest-cov测试覆盖率是衡量测试完备性的重要指标尽管不是唯一指标。pytest-cov插件让这件事变得简单。pip install pytest-cov # 运行测试并计算覆盖率 pytest --covmy_package tests/ # 生成详细的HTML覆盖率报告 pytest --covmy_package --cov-reporthtml tests/--cov参数指定要计算覆盖率的源代码包或模块。生成的HTML报告通常放在htmlcov/目录可以让你直观地看到哪些代码行被测试覆盖了哪些没有点击文件还能看到具体的未覆盖行。注意事项不要盲目追求100%的覆盖率。覆盖率高固然好但更重要的是测试的质量和有效性。有些代码如简单的getter/setter、配置常量可能不值得专门测试。覆盖率应该作为一个发现测试盲点的工具而不是一个必须达成的硬性目标。6.3 模拟与打桩unittest.mock与pytest-mock单元测试的核心原则之一是“隔离”。我们需要将被测单元与其依赖如网络请求、数据库、文件系统隔离开。Python标准库提供了unittest.mock模块而pytest通过monkeypatchfixture和pytest-mock插件提供了更集成的体验。pytest-mock插件提供了一个mockerfixture它实际上是unittest.mockAPI的一个包装器但使用起来更符合pytest的风格。import pytest from my_module import send_email, ExternalService def test_send_email(mocker): # 使用mocker.patch来模拟一个函数或对象 mock_smtp mocker.patch(‘my_module.smtplib.SMTP’) # 模拟SMTP类 mock_instance mock_smtp.return_value # 获取模拟的实例 # 调用被测函数 send_email(“toexample.com”, “Subject”, “Body”) # 断言模拟对象被以正确的方式调用 mock_smtp.assert_called_once_with(‘smtp.example.com’, 587) mock_instance.starttls.assert_called_once() mock_instance.login.assert_called_once_with(‘user’, ‘pass’) mock_instance.sendmail.assert_called_once() mock_instance.quit.assert_called_once() def test_service_integration(mocker): # 模拟一个外部服务的响应 mock_response mocker.Mock() mock_response.status_code 200 mock_response.json.return_value {‘data’: ‘test’} mocker.patch.object(ExternalService, ‘get_data’, return_valuemock_response) result ExternalService().fetch_something() assert result[‘data’] ‘test’monkeypatchfixture是pytest自带的更适合临时修改属性、环境变量或字典项。def test_with_monkeypatch(monkeypatch): import os # 临时修改环境变量 monkeypatch.setenv(“API_KEY”, “fake-key”) assert os.environ[“API_KEY”] “fake-key” # 临时修改一个对象的属性 class SomeClass: value “original” obj SomeClass() monkeypatch.setattr(obj, “value”, “patched”) assert obj.value “patched” # 测试结束后所有修改都会自动恢复选择mocker还是monkeypatchmocker提供了完整的Mock/MagicMock/patch功能适合复杂的模拟场景。monkeypatch更轻量适合简单的属性或环境变量修改。7. 常见问题排查与性能调优实战7.1 测试失败排查三板斧当测试失败时不要慌张按以下步骤排查1. 查看详细输出首先确保你使用了-v详细和-s禁用捕获显示打印输出选项。pytest -v -s test_file.py。这能让你看到测试执行过程中的所有打印信息对于调试至关重要。2. 使用--tb选项控制回溯信息长度默认情况下pytest会显示完整的错误回溯--tbauto。有时这太长了。你可以使用--tbshort只显示断言失败那一行的回溯。--tbline每个失败只显示一行。--tbno不显示回溯。 在CI环境中为了日志清晰我常用--tbshort。3. 使用pdb进行交互式调试在运行命令时加上--pdb当测试失败时会自动跳入pdbPython调试器交互环境。你可以在这里检查当时的变量状态、执行任意代码是定位复杂问题的终极武器。对于特定的测试也可以在代码中直接插入import pdb; pdb.set_trace()来设置断点。7.2 测试性能优化策略随着测试套件增长运行时间可能成为瓶颈。以下是一些优化策略1. 使用Fixture作用域如前所述将昂贵的准备工作如启动数据库、浏览器提升到session或module级别可以避免重复开销。2. 并行执行安装pytest-xdist插件。pip install pytest-xdist pytest -n auto # 使用与CPU核心数相同的worker并行运行并行化能极大缩短总运行时间尤其适合那些I/O密集型或彼此独立的测试。但要注意并行时测试执行顺序是不确定的必须确保测试之间没有依赖或状态共享。3. 标记并跳过慢测试用pytest.mark.slow标记那些运行时间很长的测试如端到端UI测试、大数据量处理测试。在开发过程中使用pytest -m “not slow”跳过它们只在 nightly build 或 CI 的完整构建中运行。4. 测试文件与目录结构优化将快速运行的单元测试和慢速运行的集成测试、UI测试分开放置在不同的目录。例如tests/ ├── unit/ # 纯逻辑单元测试运行极快 ├── integration/ # 涉及外部服务的集成测试 └── e2e/ # 端到端UI测试运行慢然后可以针对性地运行pytest tests/unit或pytest tests/integration。5. 避免在导入时或Fixture中执行昂贵操作有时我们会在模块顶层或Fixture函数体yield之前执行一些耗时操作。如果这个Fixture作用域是session且某些测试用不到它就会造成浪费。可以考虑使用惰性加载将昂贵操作移到真正需要的时候。或者使用pytest的pytest.fixture(scope“session”, autouseFalse)并只在需要的测试中请求它而不是用autouseTrue。7.3 与IDE的集成VS Code与PyCharm良好的IDE集成能提升开发效率。VS Code安装Python扩展后VS Code能自动发现pytest测试。你可以在测试文件侧边看到“运行测试”和“调试测试”的按钮。在.vscode/settings.json中配置{ “python.testing.pytestEnabled”: true, “python.testing.pytestArgs”: [ “tests”, “-v”, “--no-header” ] }PyCharmPyCharm对pytest的支持是原生级的。在Settings/Preferences - Tools - Python Integrated Tools中将Default test runner设置为pytest。之后你可以在测试方法旁边看到绿色的运行箭头可以直接运行或调试单个测试、整个类或整个文件。PyCharm还能很好地解析parametrize和fixture。无论用哪种IDE都建议在项目根目录配置好pytest.ini这样IDE在运行测试时会自动读取这些配置保证命令行和IDE内行为一致。8. 构建健壮的测试套件架构与模式8.1 测试目录结构组织一个清晰的结构有助于维护。对于中型以上项目我推荐以下结构my_project/ ├── src/ # 源代码 │ └── my_package/ │ ├── __init__.py │ ├── module_a.py │ └── module_b.py ├── tests/ # 测试代码 │ ├── unit/ # 单元测试 │ │ ├── __init__.py │ │ ├── conftest.py # 本目录及子目录共享的fixture │ │ ├── test_module_a.py │ │ └── test_module_b.py │ ├── integration/ # 集成测试 │ │ ├── conftest.py │ │ └── test_api.py │ └── e2e/ # 端到端测试 │ ├── conftest.py │ └── test_ui.py ├── conftest.py # 全局共享的fixture可选 ├── pytest.ini # 配置文件 └── requirements-test.txt # 测试专用依赖关键点src布局使用src目录将项目源代码与测试代码物理隔离避免导入混淆。conftest.py文件这是pytest的魔法文件。其中定义的Fixture可以被该文件所在目录及其所有子目录中的测试文件自动发现和使用。通常在tests/根目录的conftest.py中定义全局fixture如日志配置在tests/unit/等子目录中定义更具体的fixture。测试专用依赖将pytest、pytest-cov、pytest-mock等测试框架和插件依赖放在requirements-test.txt中与生产环境依赖requirements.txt分开。8.2 数据驱动测试的进阶模式对于需要大量测试数据的场景如测试各种边界条件的输入将测试数据放在代码里会显得臃肿。可以将数据外置到文件中。使用JSON/YAML文件# test_data.json [ {“input”: {“a”: 1, “b”: 2}, “expected”: 3}, {“input”: {“a”: -1, “b”: 1}, “expected”: 0}, {“input”: {“a”: 0, “b”: 0}, “expected”: 0} ]import json import pytest def load_test_data(): with open(‘test_data.json’, ‘r’) as f: return json.load(f) pytest.mark.parametrize(“data”, load_test_data()) def test_with_external_data(data): result add(data[“input”][“a”], data[“input”][“b”]) assert result data[“expected”]使用pytest的pytest.fixture(params…)从文件加载这可以将数据加载逻辑封装在fixture中使测试函数更简洁。8.3 测试固件Test Doubles策略在单元测试中我们常用测试固件Test Doubles来替代真实依赖。除了之前提到的Mock还有几种常见策略Fake对象实现一个轻量级的、功能简化但行为正确的替代品。例如用一个内存字典实现的FakeUserRepository替代真实的数据库仓库。class FakeUserRepository: def __init__(self): self._users {} def add(self, user): self._users[user.id] user def get(self, user_id): return self._users.get(user_id) pytest.fixture def user_repo(): return FakeUserRepository() def test_user_service(user_repo): service UserService(user_repo) service.create_user(“Alice”) assert user_repo.get(1).name “Alice”Fake对象比Mock更能测试业务逻辑的交互因为它有真实的行为。Stub提供预定义响应的对象。通常用Mock或一个简单类实现。def test_with_stub(mocker): stub_api mocker.Mock() stub_api.get_user.return_value {“id”: 1, “name”: “Stub User”} # 使用stub_api进行测试...契约测试对于服务间集成除了Mock还可以考虑使用契约测试如pact确保消费者和提供者之间的接口约定一致。这超出了pytest本身的范围但pytest可以作为其测试运行器。选择哪种策略取决于测试的层次和目的。单元测试多用Mock和Fake集成测试会使用真实依赖或更真实的替代品如测试数据库契约测试用于服务边界。9. 踩坑实录与经验之谈写了这么多年测试有些坑是反复踩过的。这里分享几个最常见的坑1Fixture的autouse陷阱pytest.fixture(autouseTrue)会让该fixture自动用于所有测试无需在测试函数参数中声明。这看起来很方便但要慎用。它会让测试的依赖关系变得隐晦难以理解。更糟糕的是如果这个autouse的fixture执行了有状态的操作比如往数据库里插了一条数据可能会意外地影响其他测试。我的原则是除非是绝对无副作用、全局必需的设置如设置一个全局的日志级别或临时路径否则不要用autouse。坑2测试执行顺序依赖pytest默认的测试发现顺序是文件系统顺序但执行顺序在文件内部是不确定的出于设计为了暴露测试间隐藏的依赖。绝对不要假设test_a会在test_b之前运行。每个测试都应该是独立的、可隔离的。如果测试间需要共享状态必须通过session或module级别的fixture来显式管理并且要确保状态可重置。坑3Mock过度或Mock了错误的东西Mock是为了隔离但Mock过度会使得测试失去意义。如果你把被测单元内部的所有调用都Mock了那你实际上什么都没测。另一个常见错误是Mock了标准库或第三方库中非常稳定、快速的部分比如datetime.datetime.now这增加了测试的复杂性却没什么收益。Mock应该主要用于那些不稳定、速度慢或有副作用的依赖如网络请求、数据库访问、文件IO。坑4不稳定的测试Flaky Tests最让人头疼的是那些时而过、时而不过的测试。常见原因有时间相关测试中使用了sleep或依赖当前时间。使用freezegun或time_machine这类库来模拟时间。并发问题测试中涉及多线程或异步操作存在竞态条件。需要仔细设计测试数据或使用同步原语。外部服务不稳定依赖的API或数据库偶尔超时。对于集成测试需要设置合理的超时和重试机制并考虑使用“测试替身”在CI中替代不稳定的外部服务。测试数据污染测试没有清理干净自己创建的数据影响了后续测试。务必使用fixture的yield或addfinalizer进行清理。一旦发现不稳定的测试应该立即标记并调查修复因为它会严重损害你对测试套件的信心。坑5忽略测试输出和日志测试不仅是验证功能也是诊断问题的工具。确保你的测试在失败时能输出足够的信息来定位问题。除了assert语句在复杂的测试流程中适当使用print或logging记录关键步骤的状态。pytest的-s选项允许你看到这些输出。对于CI可以将日志输出到文件。最后关于pytest的学习官方文档docs.pytest.org非常优秀是首选资料。遇到问题时多看看-v输出的详细信息和失败回溯大多数问题都能自己找到答案。记住好的测试和好的产品代码一样需要精心设计和维护。pytest给了你一套强大的工具但如何用好它们取决于你对测试本身的理解。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻