1. 项目概述:为什么你需要一个专业的pytest测试项目指南

如果你是一名Python开发者,无论你是刚入门的新手,还是已经写过一些 unittest 的老手,当你开始接触一个稍具规模的项目时,很快就会发现:写测试本身不难,难的是把测试写得高效、可维护,并且能融入整个开发流程。这就是为什么一个结构清晰、约定俗成的“pytest测试项目指南”如此重要。它不仅仅教你 pytest 的语法,更是教你如何用测试驱动设计、如何组织测试代码、如何利用各种插件提升效率,最终构建一个坚如磐石的软件质量保障体系。

我见过太多项目,测试文件散落各处, conftest.py 里塞满了不知所谓的 fixture ,运行一次测试要等好几分钟。这通常是因为从一开始就缺乏一个顶层的设计。一个好的测试项目,应该像你的生产代码一样,有清晰的结构、明确的职责和高效的执行路径。 pytest 框架本身非常灵活,但“能力越大,责任越大”,如果没有良好的实践指南,很容易陷入混乱。

本文将从一个真实的、可复现的Python项目(比如一个简单的ETL数据管道)出发,带你一步步搭建一个专业的pytest测试项目。我们会涵盖从项目结构、测试编写、依赖模拟(Mock)、参数化测试、覆盖率检查,到代码风格检测和自动化测试流水线(tox)的完整闭环。我的目标是,你读完这篇文章后,不仅能复制出一个标准的测试项目骨架,更能理解每一个决策背后的“为什么”,从而有能力为你自己的项目定制最合适的测试策略。

2. 测试项目的顶层设计与核心思路

在动手写第一行测试代码之前,我们必须想清楚这个测试项目要达成什么目标。对于大多数Python应用,测试的核心目标无外乎这几点:验证功能正确性(这是根本)、防止回归错误(改了这里,那里别坏)、作为活文档(测试即用例)、以及为重构提供信心。基于这些目标,我们的测试项目设计需要遵循几个核心原则。

2.1 测试隔离与可重复性

这是测试的黄金法则。每一个测试用例都应该是独立的,不依赖于外部环境(如数据库、网络服务)的特定状态,也不受其他测试用例执行结果的影响。 pytest 默认的运行机制保证了测试函数的独立性,但我们需要在代码层面主动维护这一点。这意味着要大量使用 fixture 来准备和清理测试数据,使用 pytest-mock 来隔离对外部服务的调用。例如,测试一个从API获取数据的函数,你应该 mock requests.get ,返回预设的模拟数据,而不是真的去调用一个可能不稳定、有速率限制的外部API。

2.2 测试结构反映代码结构

一个好的实践是让测试代码的结构镜像生产代码的结构。如果你的源代码放在 src/myapp/ 下,里面有 core/ utils/ api/ 等模块,那么你的 tests/ 目录下最好也有对应的 test_core/ test_utils/ test_api/ 。这样做的好处非常直观:找测试容易,维护关系清晰。对于 src/myapp/core/calculator.py ,其对应的测试文件就是 tests/test_core/test_calculator.py 。这种一一对应的关系,让新加入团队的开发者也能快速定位。

2.3 利用配置与共享设施(conftest.py)

conftest.py pytest 的精髓之一,它是一个用于存放 fixture 和插件配置的本地文件。 pytest 会自动发现项目根目录和各级子目录下的 conftest.py 。我们可以利用它来实现不同层次的 fixture 共享。比如,在项目根目录的 conftest.py 中定义整个项目都可能用到的 fixture ,如数据库连接、HTTP客户端模拟;在 tests/api/ 子目录下的 conftest.py 中,定义专门用于API测试的 fixture ,如认证token。这避免了 fixture 的重复定义,也使得测试代码更加模块化。

2.4 追求高覆盖率,但更关注有效覆盖

测试覆盖率(Coverage)是一个重要的量化指标,它能告诉你有多少代码被测试执行过。 pytest-cov 插件可以很方便地生成覆盖率报告。我们应该追求高覆盖率(比如90%以上),但必须清醒地认识到,覆盖率只是一个必要条件,而非充分条件。覆盖了100%的代码行,不代表没有bug。我们的关注点应该放在“有效覆盖”上,即测试是否覆盖了核心的业务逻辑、边界条件和异常路径。一个只测试了 if 语句 True 分支而没测 False 分支的测试,即使覆盖率显示行已覆盖,其价值也是打折扣的。

3. 搭建测试项目骨架与核心工具链

现在,让我们从一个干净的Python项目开始,一步步搭建起测试环境。假设我们的项目名为 example_etl ,是一个简单的数据提取、转换、加载管道。

3.1 初始化项目与依赖管理

首先,使用 poetry (或 pipenv , 这里以 poetry 为例)初始化项目并管理依赖。 poetry 能很好地隔离开发依赖和生产依赖。

# 初始化项目
poetry new example_etl --src
cd example_etl

# 添加核心开发依赖:pytest及常用插件
poetry add --group dev pytest pytest-mock pytest-cov

这里我们一次性添加了三个核心插件:

  • pytest : 测试框架本体。
  • pytest-mock : 提供了 mocker fixture,用于模拟对象,比标准库的 unittest.mock 更贴合 pytest 风格。
  • pytest-cov : 用于生成测试覆盖率报告。

3.2 创建标准的项目目录结构

一个清晰的结构是成功的一半。我推荐如下结构:

example_etl/
├── pyproject.toml          # 项目配置和依赖声明
├── README.md
├── src/                    # 生产代码
│   └── example_etl/
│       ├── __init__.py
│       ├── core/          # 核心业务模块
│       └── utils/         # 工具函数
└── tests/                  # 测试代码
    ├── __init__.py
    ├── conftest.py        # 项目级共享fixture
    ├── test_core/         # 对应src/example_etl/core/
    └── test_utils/        # 对应src/example_etl/utils/

注意 tests/ 目录下也有 __init__.py ,这将其变为一个Python包,在某些工具(如某些覆盖率工具)下工作得更好。 src 布局(将包放在 src 下)能避免在开发时无意中从当前目录导入模块而非安装的包,这是一个最佳实践。

3.3 编写第一个conftest.py

tests/conftest.py 中,我们可以定义一些最基础的、全局可用的 fixture 。例如,一个用于模拟 click.CliRunner fixture ,因为很多命令行工具测试会用到它。

# tests/conftest.py
import tempfile
import pytest
from click.testing import CliRunner

@pytest.fixture
def clicker():
    """提供一个CliRunner实例,用于测试click命令行应用。"""
    return CliRunner()

@pytest.fixture
def temp_json_file():
    """创建一个临时的JSON文件,并在测试后自动清理。"""
    import json
    with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
        json.dump({"test": "data"}, f)
        temp_path = f.name
    yield temp_path
    # 测试结束后,清理临时文件
    import os
    os.unlink(temp_path)

fixture 使用 yield 语句, yield 之前是设置代码,之后是清理代码。 pytest 会确保清理代码无论测试成功与否都会执行。

3.4 配置pytest运行选项

pyproject.toml (或单独的 pytest.ini )中配置 pytest ,可以让团队所有成员使用统一的测试行为。

# pyproject.toml 的 [tool.pytest.ini_options] 部分
[tool.pytest.ini_options]
testpaths = ["tests"]  # 告诉pytest在哪里找测试
addopts = [
    "-v",              # 详细输出
    "--strict-markers", # 严格检查marker,避免拼写错误
    "--tb=short",      # 错误回溯信息简短模式,更清晰
]
python_files = "test_*.py"  # 测试文件命名模式
python_classes = "Test*"    # 测试类命名模式
python_functions = "test_*" # 测试函数命名模式

[tool.pytest.ini_options.markers]
slow: "标记运行缓慢的测试(可通过 `pytest -m 'not slow'` 跳过)"
integration: "标记集成测试"

4. 深入pytest测试编写:从单元测试到集成测试

有了项目骨架,我们开始编写真正的测试。我们将遵循“测试金字塔”理念,从底层的单元测试开始。

4.1 编写坚实的单元测试

单元测试的目标是隔离地测试一个函数、一个方法或一个类的行为。我们以 src/example_etl/transformer/strip.py 中的一个字符串处理类为例。

# src/example_etl/transformer/strip.py
class StripTransformer:
    def __init__(self, config):
        self.config = config

    def transform(self, data: str) -> str:
        """去除字符串两端的空白字符。"""
        if not isinstance(data, str):
            raise ValueError("Input data must be a string")
        return data.strip()

对应的单元测试文件 tests/test_transformer/test_strip.py

# tests/test_transformer/test_strip.py
import pytest
from example_etl.transformer.strip import StripTransformer

# 使用fixture来创建被测试对象,避免重复代码
@pytest.fixture
def strip_transformer(mocker):
    # mocker fixture 由 pytest-mock 提供
    mock_config = mocker.MagicMock()
    return StripTransformer(mock_config)

# 测试正常功能
def test_strip_transformer_removes_spaces(strip_transformer):
    result = strip_transformer.transform("  hello  ")
    assert result == "hello"

# 参数化测试:用一组输入输出高效测试多种情况
@pytest.mark.parametrize(
    "input_data, expected_output",
    [
        ("  hello  ", "hello"),
        ("hello", "hello"),
        ("\t\nhello\r\n", "hello"),
        ("", ""),
    ]
)
def test_strip_transformer_various_inputs(strip_transformer, input_data, expected_output):
    result = strip_transformer.transform(input_data)
    assert result == expected_output

# 测试异常路径:输入非字符串应抛出异常
def test_strip_transformer_invalid_input(strip_transformer):
    with pytest.raises(ValueError, match="Input data must be a string"):
        strip_transformer.transform(123)

关键点解析:

  1. 使用 fixture 创建测试对象 strip_transformer fixture封装了对象的创建逻辑。如果未来 StripTransformer 的构造函数发生变化,只需修改这一处fixture。
  2. 参数化测试 @pytest.mark.parametrize :这是 pytest 的杀手锏之一。它将多组测试数据注入到一个测试函数中,极大地减少了重复代码,让测试用例更清晰。
  3. pytest.raises 上下文管理器 :用于断言代码块会抛出特定的异常。 match 参数可以进一步断言异常信息中包含特定文本。
  4. 断言使用Python原生 assert pytest 会重写 assert 语句,在断言失败时提供极其丰富的上下文信息,这是它比 unittest self.assertEqual() 更友好的地方。

4.2 使用Mock隔离依赖

单元测试要求“隔离”。当你的函数调用了数据库、网络请求、文件系统或其他模块时,你需要用 Mock 对象来替换这些依赖。 pytest-mock 提供的 mocker fixture让这一切变得简单。

假设我们有一个从外部服务获取数据的函数:

# src/example_etl/extractor/api.py
import requests

class ApiExtractor:
    def __init__(self, config):
        self.config = config
        self.api_url = config.API_URL

    def extract(self):
        response = requests.get(self.api_url)
        response.raise_for_status()
        return response.json()

测试这个类时,我们绝不能真的发起网络请求。测试如下:

# tests/test_extractor/test_api.py
import pytest
from example_etl.extractor.api import ApiExtractor

def test_api_extractor_success(mocker):
    # 1. 准备模拟数据
    mock_config = mocker.MagicMock()
    mock_config.API_URL = "https://api.example.com/data"
    expected_data = {"key": "value"}

    # 2. Mock掉requests.get
    mock_response = mocker.MagicMock()
    mock_response.json.return_value = expected_data
    mock_response.raise_for_status = mocker.MagicMock()  # 这个方法不应该做任何事
    mock_get = mocker.patch('example_etl.extractor.api.requests.get', return_value=mock_response)

    # 3. 执行测试
    extractor = ApiExtractor(mock_config)
    result = extractor.extract()

    # 4. 验证行为
    # 4.1 验证调用了正确的URL
    mock_get.assert_called_once_with("https://api.example.com/data")
    # 4.2 验证返回了正确的数据
    assert result == expected_data
    # 4.3 验证raise_for_status被调用(确保错误处理逻辑被触发)
    mock_response.raise_for_status.assert_called_once()

def test_api_extractor_failure(mocker):
    mock_config = mocker.MagicMock()
    mock_config.API_URL = "https://api.example.com/data"

    # Mock一个会抛出异常的requests.get
    mock_get = mocker.patch('example_etl.extractor.api.requests.get')
    mock_get.side_effect = requests.exceptions.ConnectionError("Network error")

    extractor = ApiExtractor(mock_config)
    # 断言异常被正确抛出(从extract方法中抛出)
    with pytest.raises(requests.exceptions.ConnectionError):
        extractor.extract()

Mock的核心技巧:

  • mocker.patch :用于替换指定命名空间下的对象。关键是要 patch 被测试代码使用它的地方 。这里我们 patch 的是 'example_etl.extractor.api.requests.get' ,而不是 'requests.get' ,因为前者是测试模块中导入的路径。
  • return_value side_effect return_value 让被mock的函数/方法返回一个固定值。 side_effect 更强大,可以是一个异常(用于测试错误处理)、一个可迭代对象(每次调用返回下一个值)或一个函数(自定义返回值逻辑)。
  • 行为断言 :使用 assert_called_once_with , assert_called_with , assert_not_called 等方法来验证mock对象是否被以预期的方式调用。这是Mock测试的精髓——验证模块间的交互协议。

4.3 组织集成测试

单元测试之上是集成测试,用于测试多个模块协同工作是否正常。例如,测试整个ETL管道的 Manage 类。

# tests/test_integration/test_manage.py
import pytest
from unittest.mock import Mock, call
from example_etl.manage import Manage
from example_etl.extractor.file import FileExtractor
from example_etl.transformer.strip import StripTransformer
from example_etl.loader.file import FileLoader

def test_full_etl_pipeline_integration(mocker, tmp_path):
    # 1. 准备真实的测试文件和目录(使用pytest内置的tmp_path fixture)
    input_file = tmp_path / "input.txt"
    input_file.write_text("  a  \n  b  \n  c  ")
    output_file = tmp_path / "output.txt"

    # 2. 创建真实的组件实例,但使用Mock配置
    mock_config = mocker.MagicMock()
    mock_config.FILE_EXTRACTOR_PATH = str(input_file)
    mock_config.FILE_LOADER_PATH = str(output_file)

    # 3. 实例化管理器并运行
    # 注意:这里我们可能选择不mock内部组件,让它们真实交互
    # 但为了测试的纯粹性,我们也可以mock掉组件的具体实现,只测试流程
    # 这里演示一个更偏向“集成”的测试:mock掉插件发现,但使用真实的转换逻辑
    mock_get_extractor = mocker.patch('example_etl.manage.get_extension')
    mock_get_transformer = mocker.patch('example_etl.manage.get_extension')
    mock_get_loader = mocker.patch('example_etl.manage.get_extension')

    mock_get_extractor.return_value = FileExtractor
    mock_get_transformer.return_value = StripTransformer
    mock_get_loader.return_value = FileLoader

    manager = Manage(config=mock_config)
    manager.run()

    # 4. 验证结果:输出文件内容应该被去除空格
    assert output_file.read_text() == "a\nb\nc"
    # 验证插件获取函数被以正确的参数调用
    mock_get_extractor.assert_called_once_with('example_etl.extractor', 'file')

集成测试的边界需要根据项目情况仔细界定。它比单元测试慢,也更脆弱(因为依赖更多真实组件),但能发现单元测试发现不了的模块间接口问题。通常建议将集成测试用 @pytest.mark.integration 标记,以便在快速开发循环中可以选择性跳过。

5. 高级技巧、配置与持续集成实践

当基础测试稳定运行后,我们需要引入更多工程化实践来提升整个测试套件的质量和效率。

5.1 测试覆盖率报告与阈值

pytest-cov 不仅可以生成报告,还可以设置覆盖率阈值,如果未达到则令测试失败,这在CI/CD流水线中非常有用。

# 运行测试并生成终端报告
pytest --cov=src/example_etl --cov-report=term-missing

# 生成HTML报告,便于详细查看
pytest --cov=src/example_etl --cov-report=html

pyproject.toml 中配置覆盖率阈值和报告格式:

[tool.coverage.run]
source = ["src/example_etl"] # 指定要统计覆盖率的源代码目录
omit = ["*/__pycache__/*", "*test*"] # 忽略的路径

[tool.coverage.report]
# 设置各覆盖率类型的失败阈值
fail_under = 90 # 总覆盖率低于90%则失败
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise AssertionError",
    "raise NotImplementedError",
    "if self.debug:",
    "if settings.DEBUG",
    "logger\\.debug",
]

5.2 使用pytest插件增强功能

pytest 生态丰富,有许多插件可以解决特定问题:

  • pytest-xdist : 并行运行测试,大幅缩短测试时间。 pytest -n auto auto 表示使用所有CPU核心)。
  • pytest-django / pytest-flask : 专门为Django/Flask框架测试提供fixture和工具。
  • pytest-asyncio : 用于测试异步 asyncio 代码。
  • pytest-bdd : 支持行为驱动开发(BDD),用Gherkin语法写测试。
  • pytest-timeout : 为测试设置超时,防止某些测试卡死。

5.3 利用tox实现多环境自动化测试

tox 是一个虚拟环境管理和测试命令行工具。它可以为你的项目在多个Python版本、多个依赖配置下自动运行测试套件。这对于确保库的兼容性至关重要。

一个基础的 tox.ini 配置:

# tox.ini
[tox]
envlist = py310, py311, py312, lint, type-check
isolated_build = true

[testenv]
# 所有测试环境共享的命令
deps =
    pytest
    pytest-mock
    pytest-cov
    pytest-xdist
commands =
    pytest -v --cov=src/example_etl --cov-report=term-missing --junitxml=junit-{envname}.xml -n auto tests/

[testenv:lint]
# 代码风格检查环境
deps =
    black
    isort
    flake8
    pylint
commands =
    black --check --diff src tests
    isort --check-only --diff src tests
    flake8 src tests
    pylint src tests --fail-under=8.0

[testenv:type-check]
# 类型检查环境(如果使用mypy)
deps =
    mypy
commands =
    mypy src

运行 tox ,它会依次为 py310 py311 py312 创建虚拟环境,安装依赖并运行测试,然后再运行 lint type-check 环境。 --junitxml 参数生成JUnit格式的报告,方便CI系统(如Jenkins, GitLab CI)集成展示。

5.4 集成到Git钩子与CI/CD

为了在代码提交前就发现问题,可以将测试和代码检查集成到Git的 pre-commit 钩子中。使用 pre-commit 框架可以方便地管理多种钩子。

.pre-commit-config.yaml 示例:

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files

  - repo: https://github.com/psf/black
    rev: 23.3.0
    hooks:
      - id: black
        args: [--safe]

  - repo: https://github.com/pycqa/isort
    rev: 5.12.0
    hooks:
      - id: isort
        args: ["--profile", "black"]

  - repo: local
    hooks:
      - id: pytest-check
        name: Run fast tests
        entry: poetry run pytest -m "not slow and not integration"
        language: system
        pass_filenames: false
        always_run: true
        stages: [commit]

在CI/CD流水线(如GitHub Actions, GitLab CI)中,配置一个任务来运行完整的 tox pytest 套件,确保合并到主分支的代码始终是健康的。

6. 常见问题、调试技巧与性能优化

即使有了完善的指南,在实际操作中还是会遇到各种问题。这里记录了一些我踩过的坑和总结的技巧。

6.1 测试执行缓慢怎么办?

测试慢是开发效率的杀手。原因和解决方案:

  • 原因1:过多的I/O或网络调用。 这是最常见的原因。 解决方案 :坚决使用Mock。对于数据库,使用内存数据库(如SQLite :memory: )或专门的测试数据库,并在 fixture 中做好数据准备和清理。对于网络请求,必须Mock。
  • 原因2:测试套件庞大,但每次运行全部。 解决方案
    • 使用 pytest-xdist 并行运行 pytest -n auto
    • 只运行修改相关的测试 :使用 pytest-picked 插件或 pytest --lf (只运行上次失败的)和 pytest --new-first (先运行新加的)。
    • 按标记(mark)选择运行 :用 @pytest.mark.slow 标记耗时测试,日常使用 pytest -m "not slow" 跳过。
  • 原因3:测试启动环境慢。 如果项目依赖庞大(如科学计算库),每次创建虚拟环境都很慢。 解决方案 :在CI中可以利用缓存(cache)来复用 pip 下载的包。本地开发可以考虑使用 pip --user 安装部分重型依赖,或者使用Docker预构建的镜像。

6.2 测试时遇到ImportError或ModuleNotFoundError

这通常是由于Python路径( sys.path )问题引起的,尤其是在使用 src 目录布局时。

  • 确保以可编辑模式安装你的包 :在项目根目录运行 pip install -e . poetry install 。这会将你的包链接到当前环境,使其可被导入。
  • pytest 配置中设置 pythonpath :在 pyproject.toml 中增加 pythonpath = ["src"] ,告诉 pytest src 目录加入 sys.path
  • 检查 conftest.py 的位置 conftest.py 中的fixture和hook只在它所在目录及其子目录中生效。确保你的 conftest.py 放在正确的位置(通常是 tests/ 根目录)。

6.3 Mock不生效?注意Patch的位置

这是Mock中最容易出错的地方。你必须 patch 被测试对象看到的地方

  • 错误示例 :你在 test_module.py import requests ,然后 mocker.patch('requests.get') 。但被测试文件 my_module.py 是从另一个地方导入的 requests (例如 from urllib import request ),那么你的patch就无效。
  • 黄金法则 :总是 patch 目标模块内部的引用。查看被测试函数的源代码,找到它 import 语句的位置,然后 patch 那个完整的路径。使用 print(mocker.patch.object) 或调试器查看patch的目标是否正确。

6.4 如何测试异步(asyncio)代码?

使用 pytest-asyncio 插件。你需要用 @pytest.mark.asyncio 标记你的异步测试函数,并在其中使用 await

import pytest
import asyncio
from myapp.async_module import async_fetch

@pytest.mark.asyncio
async def test_async_fetch(mocker):
    mock_session = mocker.AsyncMock() # 注意使用AsyncMock
    mock_session.get.return_value.__aenter__.return_value.json = mocker.AsyncMock(return_value={'data': 'test'})
    result = await async_fetch(mock_session, 'http://example.com')
    assert result == {'data': 'test'}

6.5 测试随机失败(Flaky Tests)

这是最令人头疼的问题之一。测试大部分时间通过,但偶尔失败。原因通常与状态残留、时间依赖、并发竞争有关。

  • 排查步骤
    1. 隔离测试 :单独运行失败的测试,看是否稳定复现。
    2. 检查fixture作用域 :默认 fixture function 作用域,每次测试都会新建。如果错误地使用了 session module 作用域,可能导致状态污染。
    3. 检查时间/随机性 :测试中是否使用了 time.sleep() datetime.now() random ?用 mocker.patch 替换它们(如 mocker.patch('time.sleep') mocker.patch('datetime.datetime.now', return_value=fixed_time) )。
    4. 检查外部依赖 :即使Mock了,也要确保Mock的行为是确定性的。 side_effect 列表是否用完了? return_value 是否每次都返回新对象(避免可变对象被修改)?
    5. 使用 pytest-flakefinder :这个插件可以让你重复运行测试多次,以暴露不稳定的测试。
  • 根本解决 :为Flaky Test添加 @pytest.mark.flaky(reruns=3) 标记(需要 pytest-rerunfailures 插件),让它失败时自动重试几次。但这只是权宜之计,最终还是要找到并修复其不稳定的根源。

6.6 测试数据库操作

测试数据库时,核心原则是 测试之间完全隔离

  • 使用内存数据库 :如SQLite :memory: 。速度极快,且天然隔离。
  • 使用事务回滚 :对于不支持内存模式的数据库(如PostgreSQL),可以在每个测试开始时开启一个事务,测试结束后回滚。 pytest-django pytest-sqlalchemy 等插件提供了这样的fixture。
  • 使用独立的测试数据库 :在CI环境中,可以创建一个专用于测试的数据库,并在每次测试运行前用 alembic (迁移工具)或原始SQL重置其结构。
  • 用fixture准备测试数据 :在 fixture 中创建测试所需的初始数据,并确保 fixture function 作用域,这样每个测试得到的数据都是全新的。
# 使用pytest + SQLAlchemy + 内存SQLite的示例
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from myapp.models import Base, User

@pytest.fixture(scope="session")
def engine():
    """创建内存数据库引擎,作用域为session(整个测试过程一次)。"""
    return create_engine("sqlite:///:memory:")

@pytest.fixture(scope="session")
def tables(engine):
    """创建所有表,作用域为session。"""
    Base.metadata.create_all(engine)
    yield
    Base.metadata.drop_all(engine)

@pytest.fixture
def db_session(engine, tables):
    """为每个测试函数提供一个独立的数据库会话,并在测试后回滚。"""
    connection = engine.connect()
    transaction = connection.begin()
    Session = sessionmaker(bind=connection)
    session = Session()
    yield session
    session.close()
    transaction.rollback()
    connection.close()

def test_create_user(db_session):
    user = User(name="Alice")
    db_session.add(user)
    db_session.commit()
    fetched_user = db_session.query(User).filter_by(name="Alice").first()
    assert fetched_user is not None
    assert fetched_user.name == "Alice"

构建一个专业的pytest测试项目,远不止是学会写 assert 语句。它是一个系统工程,涉及项目结构设计、依赖管理、测试策略(单元、集成)、Mock技术、覆盖率管理、代码风格、以及自动化流水线。其最终目的,是建立一个快速、可靠、可维护的安全网,让你在修改代码时充满信心,在重构系统时步履稳健,在交付产品时心中有底。这套实践并非一成不变,你需要根据项目的规模、团队的习惯和技术的演进不断调整。但万变不离其宗的核心,始终是那几条:隔离、重复、快速、清晰。当你把这些原则融入到日常的测试编写中时,你会发现,写测试不再是一项枯燥的负担,而是驱动你设计出更好代码的催化剂。

更多推荐