3. pytest固件(Fixture):从入门到实战

说实话,我刚接触pytest那会儿,最让我头疼的就是测试数据准备和清理。每个测试用例都要重复写setup和teardown,代码又臭又长。直到我真正理解了Fixture——嗯,这玩意儿简直就是测试界的瑞士军刀。

3.1 Fixture概念与作用

Fixture,说白了就是测试的"前置条件"和"后置清理"。你想想看,我们写测试时经常需要:

  • 创建数据库连接
  • 准备测试数据文件
  • 模拟用户登录状态
  • 启动某个服务

这些重复劳动,Fixture全帮你包了。它的核心作用就三个:

  1. 数据准备:测试前把环境搭好
  2. 资源共享:多个测试用例共用同一份数据
  3. 自动清理:测试完帮你擦屁股

核心要点:Fixture是pytest的依赖注入机制。测试函数声明需要什么参数,pytest就自动给你注入什么。

来看个最简单的例子:

import pytest

@pytest.fixture
def db_connection():
    # 这里假装连接数据库
    conn = {"host": "localhost", "port": 3306}
    return conn

def test_query(db_connection):
    # 直接用,不用自己连
    assert db_connection["host"] == "localhost"

我在项目中遇到过不少新手,喜欢在每个测试函数里手动创建连接。结果代码里全是重复的样板代码,维护起来想哭。用Fixture之后,清爽多了。

3.2 scope参数详解

Fixture的scope参数,决定了它的生命周期。我刚开始用的时候也搞混过,后来画了个图才彻底明白。

Fixture Scope 生命周期 session:整个测试会话只执行一次 module:每个模块执行一次 class:每个测试类执行一次 function(默认):每个测试函数执行一次

scope有四个可选值,我整理了个表格方便对比:

scope值 执行时机 适用场景 我的建议
function 每个测试函数执行前后 大部分通用场景 默认选项,安全可靠
class 每个测试类执行前后 类内测试共享资源 适合集成测试
module 每个.py文件执行前后 模块级配置加载 注意数据污染
session 整个测试运行前后 全局配置、数据库连接 慎用!容易出问题

避坑指南:我曾经在项目里把数据库连接的fixture设成了session级别,结果跑完一批测试后数据库状态变了,后面的测试全挂了。scope越大,共享风险越高,一定要想清楚再选。

3.3 yield与teardown

早期的pytest版本,fixture只能做setup。后来加了yield,才真正实现了setup+teardown的完整闭环。

yield的用法其实很简单:

@pytest.fixture
def temp_file():
    # setup:创建临时文件
    file = open("/tmp/test.txt", "w")
    file.write("hello")
    file.flush()
    
    # yield:把资源交给测试函数
    yield file
    
    # teardown:测试结束后清理
    file.close()
    import os
    os.remove("/tmp/test.txt")

def test_read(temp_file):
    content = temp_file.read()
    assert content == "hello"

为什么会这样设计?你想想看,yield之前的代码是前置准备,yield之后的代码是后置清理。中间用yield把控制权交给测试函数,测试跑完自动回来执行清理。

我个人习惯把yield和上下文管理器结合使用,代码更优雅:

@pytest.fixture
def managed_file():
    with open("/tmp/test.txt", "w") as f:
        f.write("hello")
        yield f
    # 退出with块时自动关闭文件

小技巧:如果teardown逻辑比较复杂,可以用多个yield fixtuer组合。每个只负责一件事,职责单一,调试起来也方便。

3.4 内置fixture(tmpdir, capsys等)

pytest自带了一堆好用的内置fixture,不用白不用。我挑几个最常用的说说:

tmpdir:临时目录管理

测试中经常需要读写文件,用完还得删。tmpdir帮你搞定一切:

def test_file_ops(tmpdir):
    # tmpdir是一个临时目录对象
    file_path = tmpdir.join("data.txt")
    file_path.write("test content")
    
    # 读取验证
    assert file_path.read() == "test content"
    # 测试结束自动删除,不用操心

我在项目中用tmpdir测试过文件上传功能,每个测试用例都有独立的临时目录,互不干扰,爽得很。

capsys:捕获标准输出

测试命令行工具或者日志输出时,capsys是神器:

def test_print_output(capsys):
    print("hello world")
    captured = capsys.readouterr()
    assert captured.out == "hello world\n"
    assert captured.err == ""

还有几个常用的内置fixture,我列个表:

内置fixture 作用 典型场景
tmpdir 创建临时目录 文件读写测试
tmp_path 返回pathlib.Path对象 现代Python项目
capsys 捕获stdout/stderr 命令行工具测试
monkeypatch 动态修改对象/环境变量 模拟外部依赖
request 获取当前测试上下文 动态fixture参数

重点提醒:内置fixture不需要import,直接当参数用就行。pytest会自动识别并注入。我第一次用的时候还傻乎乎地去查import路径,后来发现根本不需要。

最后分享一个组合使用的例子,把今天学的知识串起来:

import pytest

@pytest.fixture(scope="module")
def db_config():
    """模块级别的数据库配置"""
    config = {"host": "localhost", "port": 3306}
    yield config
    # 模块测试结束后清理
    print("清理数据库配置")

@pytest.fixture
def test_data(tmpdir, db_config):
    """使用内置fixture和自定义fixture"""
    data_file = tmpdir.join("test_data.json")
    data_file.write(str(db_config))
    yield data_file
    # 临时文件自动清理

def test_data_loading(test_data):
    content = test_data.read()
    assert "localhost" in content

你看,fixture之间可以互相依赖,内置的和自定义的混着用,pytest会自动帮你处理好依赖顺序。这就是我为什么说fixture是pytest的灵魂——它让测试代码变得干净、可维护、可复用。

嗯,这一章的内容就到这里。记住一句话:好的fixture设计,能让你的测试代码减少一半。下一章我们聊聊参数化测试,到时候再会。