# Pytest 完整使用指南 Pytest 是 Python 最流行的测试框架,以其简洁的 API 和强大的功能被广泛应用于各种项目中。 ## 目录 1. [快速开始](#1-快速开始) 2. [编写测试](#2-编写测试) 3. [Fixtures](#3-fixtures) 4. [异步测试](#4-异步测试) 5. [Mocking](#5-mocking) 6. [参数化测试](#6-参数化测试) 7. [跳过测试](#7-跳过测试) 8. [测试夹具作用域](#8-测试夹具作用域) 9. [测试组织](#9-测试组织) 10. [常用命令](#10-常用命令) 11. [配置文件](#11-配置文件) --- ## 1. 快速开始 ### 安装 ```bash pip install pytest pytest-asyncio ``` ### 运行测试 ```bash # 运行所有测试 pytest # 运行指定文件 pytest tests/test_api.py # 运行指定测试函数 pytest tests/test_api.py::test_health_check # 显示详细输出 pytest -v # 显示打印内容 pytest -s # 在第一个失败处停止 pytest -x ``` --- ## 2. 编写测试 ### 基本结构 ```python # test_example.py def test_basic_assertion(): """简单的断言测试""" assert 1 + 1 == 2 def test_string_operations(): """字符串操作测试""" text = "hello world" assert text.upper() == "HELLO WORLD" assert text.startswith("hello") assert len(text) == 11 def test_list_operations(): """列表操作测试""" items = [1, 2, 3] assert len(items) == 3 assert items.pop() == 3 assert items == [1, 2] ``` ### 测试类 ```python class TestMathOperations: def test_addition(self): assert 2 + 3 == 5 def test_subtraction(self): assert 5 - 3 == 2 def test_multiplication(self): assert 3 * 4 == 12 class TestStringOperations: def test_reverse(self): assert "hello"[::-1] == "olleh" ``` --- ## 3. Fixtures Fixtures 是 pytest 最强大的特性之一,用于提供测试所需的依赖和数据。 ### 基本 Fixture ```python import pytest @pytest.fixture def sample_data(): """返回一个简单的数据字典""" return {"name": "test", "value": 100} def test_using_fixture(sample_data): """使用 fixture 的测试""" assert sample_data["value"] > 50 ``` ### Fixture with Setup/Teardown ```python import pytest import tempfile import shutil @pytest.fixture def temp_directory(): """创建临时目录,测试后清理""" temp_path = tempfile.mkdtemp() yield temp_path # 测试在此运行 shutil.rmtree(temp_path, ignore_errors=True) # 清理 def test_write_file(temp_directory): """使用临时目录的测试""" import os file_path = os.path.join(temp_directory, "test.txt") with open(file_path, "w") as f: f.write("hello") assert os.path.exists(file_path) # 测试结束后 temp_path 会被自动清理 ``` ### Fixture 参数化 ```python @pytest.fixture def user_data(): return {"username": "testuser", "email": "test@example.com"} def test_user_creation(user_data): assert user_data["username"] == "testuser" ``` --- ## 4. 异步测试 ### 安装异步支持 ```bash pip install pytest-asyncio ``` ### 配置 pytest.ini ```ini [pytest] asyncio_mode = auto ``` ### 编写异步测试 ```python import pytest @pytest.mark.asyncio async def test_async_operation(): """异步测试函数""" import asyncio async def fetch_data(): await asyncio.sleep(0.1) return {"data": "result"} result = await fetch_data() assert result["data"] == "result" @pytest.mark.asyncio async def test_async_with_db(): """模拟异步数据库操作""" import asyncio async def connect_db(): await asyncio.sleep(0.1) return "database_connected" result = await connect_db() assert "connected" in result ``` ### Fixture 返回协程 ```python import pytest import asyncio @pytest.fixture async def async_database(): """异步 fixture - 连接数据库""" await asyncio.sleep(0.1) # 模拟连接 db = {"connected": True} yield db # Teardown await asyncio.sleep(0.05) @pytest.mark.asyncio async def test_database_query(async_database): assert async_database["connected"] is True ``` --- ## 5. Mocking ### 使用 unittest.mock ```python from unittest.mock import Mock, patch, AsyncMock import pytest def test_mock_function(): """Mock 函数调用""" mock = Mock(return_value=42) result = mock() assert result == 42 mock.assert_called_once() def test_patch_environment(): """Patch 环境变量""" with patch("os.environ", {"TEST": "value"}): import os assert os.environ.get("TEST") == "value" def test_mock_class(): """Mock 类""" mock_class = Mock() mock_class.method.return_value = "mocked" result = mock_class.method() assert result == "mocked" @pytest.mark.asyncio async def test_async_mock(): """Mock 异步函数""" mock_async = AsyncMock(return_value="async result") result = await mock_async() assert result == "async result" @pytest.mark.asyncio async def test_patch_async(): """Patch 异步函数""" async def real_async_func(): return "real" with patch("__main__.real_async_func", new_callable=AsyncMock) as mock_func: mock_func.return_value = "mocked" result = await mock_func() assert result == "mocked" ``` ### Mocking 第三方库 ```python from unittest.mock import patch, MagicMock import pytest # Mock HTTP 请求 @pytest.mark.asyncio async def test_api_call(): mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"status": "ok"} with patch("httpx.AsyncClient") as mock_client: mock_instance = AsyncMock() mock_instance.get = AsyncMock(return_value=mock_response) mock_client.return_value = mock_instance # 实际测试代码会使用 httpx.AsyncClient client = mock_client.get("http://api.example.com") # ... ``` --- ## 6. 参数化测试 一个测试函数使用不同参数多次运行。 ### @pytest.mark.parametrize ```python import pytest @pytest.mark.parametrize("input,expected", [ (1, 2), (2, 4), (3, 6), ]) def test_double(input, expected): """测试倍数函数""" assert input * 2 == expected @pytest.mark.parametrize("a,b,result", [ (1, 1, 2), (2, 3, 5), (10, 20, 30), ]) def test_addition(a, b, result): assert a + b == result # 字符串参数 @pytest.mark.parametrize("text,upper", [ ("hello", "HELLO"), ("World", "WORLD"), ("pytest", "PYTEST"), ]) def test_uppercase(text, upper): assert text.upper() == upper ``` ### 多参数组合 ```python import pytest @pytest.mark.parametrize("x", [1, 2, 3]) @pytest.mark.parametrize("y", [10, 20]) def test_combinations(x, y): """x=1,2,3 与 y=10,20 的所有组合""" assert x * y > 0 # 运行 6 次: (1,10), (1,20), (2,10), (2,20), (3,10), (3,20) ``` ### 使用 pytest 对象的 param ```python import pytest @pytest.fixture def pytest_param_user(): return pytest.param("user1", marks=pytest.mark.slow) def test_user_stuff(): pass ``` --- ## 7. 跳过测试 ### 跳过条件 ```python import pytest import sys @pytest.mark.skip(reason="功能未实现") def test_not_implemented(): pass @pytest.mark.skipif(sys.version_info < (3, 8), reason="需要 Python 3.8+") def test_new_feature(): pass # 动态跳过 def test_conditionally(): if some_condition: pytest.skip("条件不满足") assert True ``` ### 预期失败 ```python @pytest.mark.xfail(reason="已知 bug") def test_known_bug(): assert False # 预期失败 ``` --- ## 8. 测试夹具作用域 Fixture 可以指定不同的作用域: ```python # function: 每个测试函数执行一次(默认) @pytest.fixture def function_fixture(): print("function scope") return "function" # class: 每个测试类执行一次 @pytest.fixture(scope="class") def class_fixture(): print("class scope") return "class" # module: 每个模块执行一次 @pytest.fixture(scope="module") def module_fixture(): print("module scope") return "module" # session: 整个测试会话执行一次 @pytest.fixture(scope="session") def session_fixture(): print("session scope") return "session" ``` --- ## 9. 测试组织 ### 目录结构建议 ``` project/ ├── tests/ │ ├── __init__.py │ ├── conftest.py # 共享 fixtures │ ├── test_api.py # API 测试 │ ├── test_models.py # 模型测试 │ ├── test_services/ # 服务测试 │ │ ├── __init__.py │ │ ├── test_session.py │ │ └── test_scheduler.py │ └── test_integration/ # 集成测试 │ └── test_workflow.py ├── src/ │ └── your_code.py └── pytest.ini ``` ### conftest.py 共享配置 ```python # tests/conftest.py import pytest @pytest.fixture def common_setup(): """所有测试共享的设置""" print("Setting up test environment") yield print("Tearing down test environment") # 自动使用某些 fixtures @pytest.fixture(autouse=True) def reset_state(): """每个测试自动重置状态""" yield ``` --- ## 10. 常用命令 ```bash # 基本运行 pytest # 运行所有测试 pytest tests/ # 运行指定目录 pytest test_file.py # 运行指定文件 pytest -k "test_name" # 按名称过滤 pytest -k "test_api" # 运行包含 "test_api" 的测试 # 输出控制 pytest -v # 详细输出 pytest -s # 显示 print 输出 pytest --tb=short # 简短 traceback pytest --tb=long # 详细 traceback # 调试 pytest -x # 第一个失败后停止 pytest --pdb # 失败时进入 debugger pytest -l # 显示失败的局部变量 # 覆盖率 pytest --cov=src # 运行并计算覆盖率 pytest --cov-report=html # 生成 HTML 报告 # 标记 pytest -m "slow" # 运行带 slow 标记的测试 pytest -m "not slow" # 排除 slow 标记 # 异步 pytest --asyncio-mode=auto # 自动检测异步测试 ``` --- ## 11. 配置文件 ### pytest.ini ```ini [pytest] minversion = 6.0 testpaths = tests python_files = test_*.py python_classes = Test* python_functions = test_* # 异步配置 asyncio_mode = auto # 标记定义 markers = slow: 耗时较长的测试 integration: 集成测试 unit: 单元测试 # 忽略路径 norecursedirs = .git .tox build dist # 自定义选项 addopts = -v --tb=short ``` ### pyproject.toml ```toml [tool.pytest.ini_options] minversion = "6.0" testpaths = ["tests"] pythonpath = ["."] [tool.pytest.markers] slow = "marks tests as slow" integration = "marks tests as integration tests" [tool.coverage.run] source = ["src"] [tool.coverage.report] exclude_lines = [ "pragma: no cover", "def __repr__", "raise AssertionError", ] ``` --- ## 高级技巧 ### 1. 自定义标记 ```python # 在代码中使用 @pytest.mark.slow def test_slow_operation(): import time time.sleep(5) assert True @pytest.mark.integration def test_api_workflow(): pass # 在 pytest.ini 中注册 [pytest] markers = slow: marks tests as slow (deselect with '-m "not slow"') integration: marks tests as integration tests ``` ### 2. 条件跳过 ```python import pytest # 基于环境变量 import os @pytest.mark.skipif( os.getenv("CI") == "true", reason="跳过 CI 环境" ) def test_dev_only(): pass # 基于平台 @pytest.mark.skipif( sys.platform == "win32", reason="不适用于 Windows" ) def test_linux_only(): pass ``` ### 3. Fixture 依赖 ```python @pytest.fixture def database(): return {"connected": False} @pytest.fixture def db_session(database): """依赖另一个 fixture""" session = {"db": database, "active": True} return session def test_database_connection(db_session): assert db_session["active"] is True ``` ### 4. 工厂 Fixture ```python @pytest.fixture def user_factory(): """工厂 fixture - 每次返回新实例""" created = [] def _create_user(name): user = {"id": len(created) + 1, "name": name} created.append(user) return user return _create_user def test_create_users(user_factory): user1 = user_factory("Alice") user2 = user_factory("Bob") assert user1["id"] == 1 assert user2["id"] == 2 ``` ### 5. 临时文件 ```python import pytest from pathlib import Path import tempfile import shutil @pytest.fixture def temp_dir(): temp_path = Path(tempfile.mkdtemp()) yield temp_path shutil.rmtree(temp_path, ignore_errors=True) def test_file_operations(temp_dir): file_path = temp_dir / "test.txt" file_path.write_text("Hello, World!") assert file_path.read_text() == "Hello, World!" ``` --- ## 测试最佳实践 1. **测试名称要描述性**: `test_user_can_login` 比 `test_login` 更好 2. **AAA 模式**: Arrange (准备) -> Act (执行) -> Assert (断言) 3. **每个测试一个关注点**: 保持测试简短和专注 4. **使用描述性断言消息**: ```python assert result == expected, f"Expected {expected}, got {result}" ``` 5. **避免测试间的依赖**: 每个测试应该独立运行 6. **保持测试快速**: 单元测试应该毫秒级完成 7. **合理使用 Mock**: 不要过度 Mock,保持测试接近真实场景 8. **定期运行测试**: 建议配置 CI/CD 自动运行 --- ## 常见问题 ### Q: 如何测试异常? ```python import pytest def test_exception(): with pytest.raises(ValueError) as exc_info: raise ValueError("error message") assert str(exc_info.value) == "error message" ``` ### Q: 如何测试警告? ```python import warnings import pytest def test_warning(): with warnings.catch_warnings(record=True) as w: warnings.warn("deprecated", DeprecationWarning) assert len(w) == 1 assert str(w[0].message) == "deprecated" ``` ### Q: 如何测试日志输出? ```python import logging import pytest def test_logging(caplog): logger = logging.getLogger("test") logger.warning("test message") assert "test message" in caplog.text ``` --- ## 资源链接 - 官方文档: https://docs.pytest.org/ - Pytest 插件索引: https://plugincompat.herokuapp.com/ - Awesome Pytest: https://github.com/pytest-dev/pytest --- > 提示: 在本项目中,所有测试文件位于 `tests/` 目录,运行 `pytest` 即可执行全部测试。