Pytest Fixture 完全指南:从零到自动化测试框架实战

在Pytest框架中fixture是一个非常重要的功能,广泛应用于测试环境搭建、测试数据管理、浏览器生命周期控制以及测试报告生成等关键环节,所以理解好fixture的使用对于测试框架的搭建是很有帮助的。

本文将系统性地介绍Pytest fixture,涵盖以下内容:

  • Pytest fixture简介:解释Pytest fixture的基本概念。
  • Fixture的作用域与执行顺序:详细说明fixture的不同作用域(如函数、类、模块、会话级别)及其执行顺序规则
  • 自动化测试中的常见 fixture 应用场景:通过实际示例展示Pytest fixture在数据准备、环境配置等方面的典型应用,提升测试自动化效率。

什么是Pytest fixture

pytest的fixture其实是一个函数,通过装饰器 @pytest.fixture 注册到 Pytest 框架中用于在测试执行前后完成准备与清理工作。

fixture的核心优势在于它的可复用性和模块化设计 。你可以将常用的测试准备逻辑(如数据库连接、文件读取、配置加载等)封装成独立的fixture,然后在测试函数中复用。

此外,fixture之间可以通过依赖的方式,一个fixture可以依赖另一个fixture,从而可以更好的管理前置处理,将多个前置操作解耦。

如何使用Pytest fixture

当测试函数的参数名与某个fixture的函数名一致时,Pytest 会自动调用该 fixture 并将fixture yield的内容自动返回给参数。

示例:

复制代码
import pytest

@pytest.fixture
def setup_and_teardown():
    print("[Setup] 初始化测试环境")
    yield "fixture"
    print("[Teardown] 清理测试环境")

def test_example(setup_and_teardown):
    print(f"执行测试函数,获得值:{setup_and_teardown}")

运行结果:

复制代码
[Setup] 初始化测试环境
执行测试函数,获得值:fixture
[Teardown] 清理测试环境
  • yield 前的代码 在测试函数执行之前运行 ,这里称为setup阶段
  • yield 后的代码 在测试函数执行之后运行 ,这里称为teardown阶段
  • yield 的内容 会作为参数传递给使用该 fixture 的测试函数 💡 如果通过装饰器 @pytest.mark.usefixtures("setup_and_teardown") 引用 fixture 且未显式通过参数注入,则 yield 返回的值不会被测试函数使用(不影响 setup 和 teardown 的执行)。

Pytest fixture执行顺序

Pytest fixture的执行顺序由以下三个规则决定:

  1. 按作用域大小从外到内:作用域大的fixture(如session、module)先于作用域小的fixture(如function)执行。
  2. 按依赖关系解析:作用域相同时,被依赖的fixture先于依赖它的fixture执行。
  3. 无依赖时按参数顺序执行:当多个fixture作用域相同且不存在依赖时,则按在测试函数中的参数顺序决定

规则1: 按作用域大小从外到内

在 pytest 中,fixture 的执行顺序主要由其作用域决定:作用域越大,优先级越高。作用域从大到小依次为:session → package → module → class → function。

各 fixture 作用域的具体说明如下表所示。

作用域 执行频率 使用场景
function 每个测试函数执行(默认作用域) 适用于独立测试,互不影响的场景
class 每个测试类前后执行 适用于测试类内共享状态(如登录信息)
module 每个模块(一个py文件)前后执行 适用于模块级共享资源(如数据库连接)
package 每个包前后执行(Pytest 7.0新增) 类似于模块级别
session 整个测试会话前后执行 适用于全局资源(如 WebDriver、全局配置)

示例

复制代码
import pytest  
  
# 1. 不同作用域的fixture  
@pytest.fixture(scope="session")  
def session_fixture():  
    print("\nSession fixture setup")  
    yield "session_data"  
    print("Session fixture teardown")  
  
  
@pytest.fixture(scope="module")  
def module_fixture():  
    print("Module fixture setup")  
    yield "module_data"  
    print("Module fixture teardown")  
  
  
@pytest.fixture(scope="function")  
def function_fixture():  
    print("Function fixture setup")  
    yield "function_data"  
    print("Function fixture teardown")  
  
  
# 测试函数  
def test_execution_order(  
        session_fixture,  # 作用域最大,最先执行  
        module_fixture,  # 作用域次之  
        function_fixture,  # 作用域最小  
):  
    print("\nTest execution")  
    assert True

运行结果如下:

复制代码
test_scope.py::test_execution_order 
Session fixture setup
Module fixture setup
Function fixture setup
PASSED                               [100%]
Test execution
Function fixture teardown
Module fixture teardown
Session fixture teardown

从结果可以看到,fixture 的调用顺序是:Session → Module → Function。而 teardown 的执行顺序则与之相反。

规则2: 按依赖关系解析

如果fixture的作用域相同时,pytest会接着根据fixture之间的互相依赖关系来决定执行顺序。

在Setup阶段,被依赖的fixture会先于依赖的fixture先执行,例如当loginfixture依赖dbfixture,则db会在login之前运行,在teardown阶段时则会相反。

通过依赖的方式,测试框架可以方便高效的管理测试需要的前置和后置处理的执行顺序

示例代码:

复制代码
@pytest.fixture
def db():
    print("\\n[setup] db")
    yield
    print("[teardown] db")

@pytest.fixture
def login(db):
    print("[setup] login")
    yield
    print("[teardown] login")

@pytest.fixture
def prepare_data(login):
    print("[setup] prepare_data")
    yield
    print("[teardown] prepare_data")

def test_api(prepare_data):
    print("执行 test_api")

运行时输出结果如下

复制代码
tests/test_class_scope.py 
[setup] db
[setup] login
[setup] prepare_data
执行 test_api
.[teardown] prepare_data
[teardown] login
[teardown] db

根据fixture的依赖关系 prepare_data -> login -> db,所以最后的执行顺序:先执行 db,然后是 login,最后是 prepare_data。测试完成后,teardown代码将按照相反顺序执行。

规则3:无依赖时按参数顺序执行

如果一个函数使用了多个fixture,且前两个条件都相同(作用域相同+没有依赖关系)。

此时Pytest会依据参数的声明顺序,从左到右执行。顺便一提,如果你的fixture 确实存在依赖关系,应显示的使用依赖机制来明确执行顺序,而非依赖参数的声明顺序。

示例

复制代码
@pytest.fixture
def db():
    print("[setup] db")
    yield
    print("[teardown] db")

@pytest.fixture
def login():
    print("[setup] login")
    yield
    print("[teardown] login")

@pytest.fixture
def prepare_data():
    print("[setup] prepare_data")
    yield
    print("[teardown] prepare_data")

def test_api(prepare_data, login, db):
    print("执行 test_api")

运行后结果如下

复制代码
tests/test_class_scope.py [setup] prepare_data
[setup] login
[setup] db
执行 test_api
.[teardown] db
[teardown] login
[teardown] prepare_data

结果显示,在 setup 阶段,pytest会按照参数声明的顺序(从左到右)执行,即 prepare_data > login > db。而在teardown阶段则按照相反的顺序执行。


自动化测试中常见的fixture应用场景

管理浏览器实例

在UI自动化测试中,创建浏览器对象是必不可少的,例如Selenium中的webdriver实例或Playwright中的page对象。使用Pytest fixture来管理和创建这些对象,在几乎所有框架中都是一项必修课,那接着展示一下如何使用pytest fixture实现通过加载配置文件进行playwright浏览器实例的创建

1 配置加载:

复制代码
import pytest
import yaml
from playwright.sync_api import sync_playwright, Browser, Page
import logging
from typing import Dict, Any

# 设置日志
logger = logging.getLogger(__name__)

def load_config(env: str = "default") -> Dict[str, Any]:
    """
    从配置文件加载配置
    
    :param env: 环境名称,对应配置文件 env.{env}.yaml
    :return: 配置字典
    """
    config_file = f"configs/env.{env}.yaml"
    try:
        with open(config_file, 'r', encoding='utf-8') as f:
            config = yaml.safe_load(f)
        logger.info(f"加载配置文件: {config_file}")
        return config or {}
    except FileNotFoundError:
        logger.warning(f"配置文件 {config_file} 不存在,使用默认配置")
        return {}

@pytest.fixture(scope="session")
def config(request):
    """
    加载当前运行环境配置
    
    :param request: pytest 请求对象,用于获取命令行参数
    :return: 配置字典
    """
    logger.info("开始加载配置")
    
    # 从命令行参数获取环境变量,如果没有则使用默认值
    env = request.config.getoption("--env", default="default")
    return load_config(env)
  • 这段代码定义了一个会话级别 (scope="session") 的fixture,通过load_config函数从对应的配置文件(如 env.dev.yaml)读取配置,根据命令行参数(如 --env=dev)加载对应环境的配置文件。
    2 浏览器实例管理(browser fixture)

    @pytest.fixture(scope="session")
    def browser(config: Dict[str, Any]):
    """
    启动并提供一个浏览器实例,测试会话结束后关闭浏览器

    复制代码
      :param config: 配置对象 fixture - 来自 conftest.py
      :return: Playwright Browser 对象
      """
      logger.info("开始测试会话: 启动浏览器")
    
      playwright = sync_playwright().start()
    
      # 从配置中获取浏览器参数,使用默认值
      browser_type = config.get("browser", "chromium")
      headless = config.get("headless", True)
      slowmo = config.get("slowmo", 0)
      
      # 额外的启动参数
      launch_kwargs = {
          "headless": headless,
          "slow_mo": slowmo
      }
      
      # 可选的浏览器特定参数
      if browser_type == "chromium" and config.get("chromium_args"):
          launch_kwargs["args"] = config.get("chromium_args")
    
      logger.info(f"启动浏览器={browser_type}, headless={headless}, slowmo={slowmo}")
    
      # 根据 browser_type 动态选择浏览器
      browser = getattr(playwright, browser_type).launch(**launch_kwargs)
      
      yield browser
      
      logger.info("关闭浏览器")
      browser_instance.close()
      playwright.stop()
  • browser fixture 依赖于上一步的 config fixture,browser fixture 通过从 config 返回的配置中读取浏览器相关设置(如 browser_type、headless 等),然后根据这些配置创建并返回浏览器对象。

  • 在teardown阶段代码(yield之后),浏览器及 Playwright 对象都会被正确关闭,避免资源泄漏。
    3 页面实例对象管理(page fixture)

    @pytest.fixture(scope="function")
    def page(browser: Browser):
    """
    为每个测试函数提供一个 Playwright Page 对象

    复制代码
      :param browser: 浏览器实例 fixture
      :return: Playwright Page 对象
      """
      # 创建新的浏览器上下文(每个测试独立的会话)
      context = browser.new_context(
          viewport={"width": 1920, "height": 1080},
          ignore_https_errors=True  # 忽略 HTTPS 错误,便于测试环境
      )
      
      # 在新上下文中创建页面
      page = context.new_page()
      
      yield page
      
      # 测试结束后关闭上下文
      logger.debug("测试结束,关闭浏览器上下文")
      context.close()
  • 这段函数定义了一个函数级别 (scope="function") 的 fixture page,依赖于上一步的 browser fixture。它为每个测试函数提供一个全新的page对象。

测试数据准备

测试离不开测试数据,使用 Pytest的fixture 进行测试数据准备也是一种常见的方法。通过fixture管理测试数据可以有效提升测试的稳定性,并确保测试之间的数据隔离,避免"脏数据"对其他测试用例造成影响。

举例

当我们需要测试登录功能时,可以在 setup 阶段调用接口预先创建一个用户,然后在 teardown 阶段将其删除。

复制代码
import pytest

def create_user(username, email):
    # 在实际项目中,这里会调用 API 或数据库操作
    user_id = f"user_{username}"
    print(f"注册用户: {username} ({email})")
    return {"id": user_id, "username": username, "email": email}

def delete_user(user_id):
    # 在实际项目中,这里会调用 API 或数据库操作
    print(f"删除用户: {user_id}")

# 数据准备和清理的 Fixture
@pytest.fixture
def registered_user():
    """在测试前注册用户,测试后自动清理"""
    # 准备阶段:注册用户
    user = create_user("test_user", "test@example.com")
    
    # 将用户数据传递给测试用例
    yield user
    
    # 清理阶段:删除用户
    delete_user(user["id"])

# 测试用例
def test_user_login(registered_user):
    """测试用户登录功能"""
    # 使用 fixture 提供的用户数据
    assert registered_user["username"] == "test_user"
    assert registered_user["email"] == "test@example.com"
    print(f"测试用户登录: {registered_user['username']}")

以上代码使用 registered_user fixture 来管理测试数据的初始化和清理:

  • 在 setup 阶段,通过 create_user 创建新用户,并将 user 对象返回给测试用例
  • 在 teardown 阶段,会自动清理该用户,避免测试数据污染

管理接口测试的认证

在接口自动化测试中,fixture可以用来处理认证获取token、设置统一的请求头等常见的场景

示例:使用fixture管理API认证Token

复制代码
import pytest
import requests
import logging
from typing import Dict, Any

logger = logging.getLogger(__name__)

@pytest.fixture(scope="session")
def api_token() -> str:
    """
    会话级别的API Token fixture:
    - setup: 调用登录接口获取Token
    - yield: 返回Token供测试使用
    """
    base_url = "http://localhost:8080"  # 可替换为 os.getenv("API_BASE_URL")
    login_url = f"{base_url}/api/login"
    payload = {
        "username": "test_user",
        "password": "test_pass123"
    }
    
    logger.info("开始登录获取API Token")
    response = requests.post(login_url, json=payload, timeout=15)
    response.raise_for_status()
    token = response.json()["access_token"]
    logger.info(f"登录成功,获取Token: {token[:20]}...")
    
    yield token
    logger.info("API Token会话结束")

@pytest.fixture(scope="function")
def api_headers(api_token):
    """
    函数级别的请求头fixture,包含认证Token和统一头信息
    依赖 api_token fixture,确保每次测试都有最新头
    """
	logger.debug("准备API请求头")
    headers = {
        "Authorization": f"Bearer {api_token}",
        "Content-Type": "application/json",
        "User-Agent": "pytest-api-test/1.0"
    }
    yield headers

# 测试用例
def test_get_user_profile(api_headers):
    """测试获取用户资料接口"""
    base_url = "http://localhost:8080"  # 或从config获取
    url = f"{base_url}/api/user/profile"
    
    response = requests.get(url, headers=api_headers)
    assert response.status_code == 200
    user_data = response.json()
    assert user_data["username"] == "test_user"
    logger.info("用户资料接口测试通过")

以上示例展示了如何使用 Pytest fixture 来管理接口测试中的认证流程:

  • 测试方法 test_get_user_profile 通过注入 api_headers fixture 获取请求头
  • api_headers fixture 调用 api_token fixture 获取 token,并将其添加到请求头的 Authorization 字段中
  • 在项目实战中,可通过配置文件(如 YAML)或数据驱动的装饰器(pytest.mark.parametrize)切换测试环境与账号。

总结

Pytest fixture是构建可维护、模块化自动化测试框架的基础。掌握好fixture的使用,不仅能让你写出更简洁的测试代码,更能帮助团队建立起一套高效、稳健的自动化测试体系。

通过本文,我们深入了解了Pytest fixture的核心机制:

  1. Pytest fixture核心概念 :fixture是一个通过 @pytest.fixture 装饰器注册的函数,用于在测试执行前后完成环境准备与资源清理。
  2. 明确的执行顺序:掌握"作用域 > 依赖关系 > 参数顺序"的优先级规则,在处理复杂的前置依赖时更游刃有余。
  3. 强大的实战能力 :无论是通过 conftest.py 结合配置文件动态启动浏览器,还是通过fixture 实现测试数据的创建和销毁,都极大地提升了测试代码的复用性和稳定性。