Pytest 中的 Fixture 详解
1. 什么是 Fixture
1.1 基本概念
Fixture(夹具) 是 pytest 中一个非常重要的概念,它提供了一种机制来为测试用例提供前置准备和后置清理的功能。你可以把 fixture 理解为测试用例的"准备工作"和"收尾工作"。
1.2 形象比喻
想象一下,你要做一道菜:
没有 Fixture 的情况:
- 每次做菜前,你都要自己去买菜、洗菜、切菜
- 每次做完菜,你都要自己洗碗、收拾厨房
- 如果要做 10 道菜,这些准备工作要重复 10 次
有 Fixture 的情况:
- 你有一个"助手"(fixture),它会自动帮你:
- 在开始做菜前准备好所有食材(前置准备)
- 在完成做菜后自动清理厨房(后置清理)
- 你只需要专注于"做菜"本身(测试逻辑)
- 无论做多少道菜,准备工作都只需要定义一次
1.3 Fixture 的作用
Fixture 主要有以下几个作用:
- 提供测试数据:为测试用例准备需要的数据
- 初始化资源:创建数据库连接、打开文件、启动服务等
- 清理资源:关闭连接、删除临时文件、停止服务等
- 共享代码:多个测试用例可以共享相同的准备和清理逻辑
- 依赖注入:将测试依赖项注入到测试函数中
1.4 为什么需要 Fixture
在实际测试中,你经常会遇到以下场景:
场景 1:数据库测试
# 没有 fixture 的情况
def test_create_user():
# 每次都要手动连接数据库
db = connect_database()
# 执行测试
result = create_user(db, "张三")
assert result is not None
# 每次都要手动关闭连接
db.close()
def test_delete_user():
# 又要重复连接数据库
db = connect_database()
# 执行测试
result = delete_user(db, "张三")
assert result is True
# 又要重复关闭连接
db.close()
问题:
- 代码重复:每个测试都要写连接和关闭的代码
- 容易出错:可能忘记关闭连接
- 维护困难:如果连接方式改变,要修改所有测试
使用 Fixture 后:
@pytest.fixture
def db_connection():
# 前置:连接数据库
db = connect_database()
yield db # 提供给测试使用
# 后置:关闭数据库
db.close()
def test_create_user(db_connection):
# 直接使用,不需要关心连接和关闭
result = create_user(db_connection, "张三")
assert result is not None
def test_delete_user(db_connection):
# 直接使用,不需要关心连接和关闭
result = delete_user(db_connection, "张三")
assert result is True
优势:
- 代码复用:连接和关闭逻辑只写一次
- 自动管理:pytest 自动处理资源的创建和清理
- 易于维护:修改连接方式只需改一个地方
2. Fixture 的基本语法
2.1 最简单的 Fixture
2.1.1 定义 Fixture
使用 @pytest.fixture 装饰器来定义一个 fixture:
import pytest
@pytest.fixture
def my_data():
"""这是一个简单的 fixture"""
data = "这是测试数据"
return data
2.1.2 使用 Fixture
在测试函数中,将 fixture 的名称作为参数传入:
def test_example(my_data):
"""使用 fixture"""
print(my_data) # 输出:这是测试数据
assert my_data == "这是测试数据"
2.1.3 完整示例
文件:test_simple_fixture.py
import pytest
# 定义 fixture
@pytest.fixture
def user_name():
"""返回一个用户名"""
return "张三"
# 使用 fixture
def test_greeting(user_name):
"""测试问候语"""
greeting = f"你好,{user_name}!"
assert greeting == "你好,张三!"
print(greeting)
def test_user_info(user_name):
"""测试用户信息"""
info = f"当前用户:{user_name}"
assert "张三" in info
print(info)
运行测试:
pytest test_simple_fixture.py -v
输出:
test_simple_fixture.py::test_greeting PASSED
test_simple_fixture.py::test_user_info PASSED
2.2 Fixture 的执行流程
2.2.1 基本执行流程
import pytest
@pytest.fixture
def setup_data():
print("【前置】准备数据")
data = [1, 2, 3, 4, 5]
return data
print("这行代码不会执行,因为 return 已经结束了函数")
def test_sum(setup_data):
print("【测试】执行测试")
result = sum(setup_data)
assert result == 15
print(f"结果:{result}")
执行流程:
- pytest 发现
test_sum需要setup_datafixture - 执行
setup_data()函数(前置准备) - 将返回值传递给
test_sum函数 - 执行
test_sum函数(测试逻辑) - 测试完成
输出:
【前置】准备数据
【测试】执行测试
结果:15
2.3 使用 yield 实现后置清理
2.3.1 yield 的作用
yield 关键字可以将 fixture 分为两部分:
- yield 之前:前置准备代码
- yield 之后:后置清理代码
import pytest
@pytest.fixture
def file_handler():
print("【前置】打开文件")
file = open("test.txt", "w", encoding="utf-8")
yield file # 提供给测试使用
print("【后置】关闭文件")
file.close()
def test_write_file(file_handler):
print("【测试】写入文件")
file_handler.write("测试内容")
file_handler.flush()
assert True
执行流程:
- 执行
file_handlerfixture 的 yield 之前部分(打开文件) - 将 file 对象传递给测试函数
- 执行测试函数(写入文件)
- 测试完成后,执行 yield 之后部分(关闭文件)
输出:
【前置】打开文件
【测试】写入文件
【后置】关闭文件
2.3.2 yield 与 return 的区别
使用 return:
@pytest.fixture
def data_with_return():
print("准备数据")
data = [1, 2, 3]
return data
print("这行不会执行") # 永远不会执行
def test_example(data_with_return):
assert len(data_with_return) == 3
使用 yield:
@pytest.fixture
def data_with_yield():
print("【前置】准备数据")
data = [1, 2, 3]
yield data
print("【后置】清理数据") # 测试完成后会执行
def test_example(data_with_yield):
assert len(data_with_yield) == 3
关键区别:
return:函数执行到 return 就结束了,后面的代码不会执行yield:函数会暂停,测试执行完后继续执行 yield 后面的代码
2.3.3 实际应用示例:数据库连接
import pytest
class Database:
def __init__(self):
self.connected = False
def connect(self):
print("连接数据库...")
self.connected = True
def close(self):
print("关闭数据库...")
self.connected = False
@pytest.fixture
def db():
"""数据库 fixture"""
database = Database()
database.connect() # 前置:连接数据库
yield database # 提供给测试使用
database.close() # 后置:关闭数据库
def test_query(db):
"""测试查询"""
assert db.connected is True
print("执行查询操作")
def test_insert(db):
"""测试插入"""
assert db.connected is True
print("执行插入操作")
运行结果:
连接数据库...
执行查询操作
关闭数据库...
连接数据库...
执行插入操作
关闭数据库...
3. Fixture 的作用域(Scope)
3.1 什么是作用域
Fixture 的作用域决定了 fixture 在什么时候被创建和销毁。pytest 提供了 5 种作用域:
- function(默认):每个测试函数执行一次
- class:每个测试类执行一次
- module:每个测试模块(文件)执行一次
- package:每个包执行一次
- session:整个测试会话执行一次
3.2 function 作用域(默认)
3.2.1 基本说明
function 是默认作用域,每个测试函数都会执行一次 fixture。
import pytest
@pytest.fixture(scope="function") # 可以省略,因为这是默认值
def counter():
print("创建计数器")
count = 0
return count
def test_add_one(counter):
counter += 1
print(f"测试1:counter = {counter}")
def test_add_two(counter):
counter += 2
print(f"测试2:counter = {counter}")
执行结果:
创建计数器
测试1:counter = 1
创建计数器
测试2:counter = 2
说明:每个测试函数都会创建一个新的 counter,它们之间互不影响。
3.2.2 使用场景
- 每个测试需要独立的数据
- 测试之间不应该相互影响
- 需要清理的资源(如临时文件)
示例:
import pytest
import os
@pytest.fixture(scope="function")
def temp_file():
"""为每个测试创建临时文件"""
filename = "temp_test.txt"
with open(filename, "w", encoding="utf-8") as f:
f.write("初始内容")
yield filename
# 后置清理
if os.path.exists(filename):
os.remove(filename)
print(f"删除临时文件:{filename}")
def test_read_file(temp_file):
with open(temp_file, "r", encoding="utf-8") as f:
content = f.read()
assert "初始内容" in content
def test_write_file(temp_file):
with open(temp_file, "w", encoding="utf-8") as f:
f.write("新内容")
with open(temp_file, "r", encoding="utf-8") as f:
content = f.read()
assert "新内容" in content
3.3 class 作用域
3.3.1 基本说明
class 作用域表示每个测试类只执行一次 fixture。
import pytest
@pytest.fixture(scope="class")
def class_setup():
print("【类级别】初始化")
data = {"users": []}
yield data
print("【类级别】清理")
class TestUserManagement:
def test_add_user(self, class_setup):
class_setup["users"].append("张三")
print(f"用户列表:{class_setup['users']}")
assert len(class_setup["users"]) == 1
def test_add_another_user(self, class_setup):
class_setup["users"].append("李四")
print(f"用户列表:{class_setup['users']}")
assert len(class_setup["users"]) == 2 # 注意:这里会是2,因为共享同一个fixture
class TestProductManagement:
def test_add_product(self, class_setup):
# 这是一个新的类,会创建新的fixture实例
class_setup["products"] = ["商品1"]
print(f"商品列表:{class_setup.get('products', [])}")
执行结果:
【类级别】初始化
用户列表:['张三']
用户列表:['张三', '李四']
【类级别】清理
【类级别】初始化
商品列表:['商品1']
【类级别】清理
说明:
TestUserManagement类中的两个测试共享同一个class_setupTestProductManagement类会创建新的class_setup实例
3.3.2 使用场景
- 一个类中的所有测试需要共享相同的初始化
- 类的测试需要共享状态
- 减少重复的初始化操作
示例:
import pytest
@pytest.fixture(scope="class")
def database():
"""为整个测试类创建数据库连接"""
print("连接数据库")
db = {"connection": "active", "tables": []}
yield db
print("关闭数据库连接")
class TestDatabaseOperations:
def test_create_table(self, database):
database["tables"].append("users")
assert len(database["tables"]) == 1
def test_create_another_table(self, database):
database["tables"].append("products")
assert len(database["tables"]) == 2 # 共享同一个database
def test_list_tables(self, database):
assert len(database["tables"]) == 2 # 可以看到前面创建的两个表
3.4 module 作用域
3.4.1 基本说明
module 作用域表示每个测试模块(文件)只执行一次 fixture。
import pytest
@pytest.fixture(scope="module")
def module_data():
print("【模块级别】初始化数据")
data = {"count": 0}
yield data
print("【模块级别】清理数据")
def test_increment_one(module_data):
module_data["count"] += 1
assert module_data["count"] == 1
def test_increment_two(module_data):
module_data["count"] += 2
assert module_data["count"] == 3 # 注意:是3,因为共享同一个fixture
def test_increment_three(module_data):
module_data["count"] += 3
assert module_data["count"] == 6 # 继续累加
执行结果:
【模块级别】初始化数据
【模块级别】清理数据
说明:整个模块中的所有测试共享同一个 module_data fixture。
3.4.2 使用场景
- 整个模块的测试需要共享资源(如数据库连接、配置文件)
- 减少重复的初始化操作
- 模块级别的设置和清理
示例:
import pytest
import json
@pytest.fixture(scope="module")
def config():
"""加载配置文件(整个模块只需要加载一次)"""
print("加载配置文件")
with open("config.json", "r", encoding="utf-8") as f:
config_data = json.load(f)
yield config_data
print("清理配置")
def test_api_url(config):
assert "api_url" in config
print(f"API地址:{config['api_url']}")
def test_database_config(config):
assert "database" in config
print(f"数据库配置:{config['database']}")
def test_other_config(config):
assert "other" in config
print(f"其他配置:{config['other']}")
3.5 session 作用域
3.5.1 基本说明
session 作用域表示整个测试会话(一次 pytest 运行)只执行一次 fixture。
文件1:test_login.py
import pytest
@pytest.fixture(scope="session")
def login_session():
print("【会话级别】用户登录")
user = {"username": "test_user", "logged_in": True}
yield user
print("【会话级别】用户登出")
def test_check_login(login_session):
assert login_session["logged_in"] is True
def test_user_info(login_session):
assert login_session["username"] == "test_user"
文件2:test_profile.py
import pytest
# 注意:这里不需要重新定义 fixture,可以从 conftest.py 导入
# 或者如果定义在同一个包中,可以直接使用
def test_update_profile(login_session): # 使用同一个 fixture
assert login_session["logged_in"] is True
login_session["profile_updated"] = True
def test_view_profile(login_session):
assert login_session.get("profile_updated") is True
执行结果(运行两个文件):
【会话级别】用户登录
(执行所有测试)
【会话级别】用户登出
说明:无论有多少个测试文件,login_session fixture 只会创建一次。
3.5.2 使用场景
- 整个测试套件需要共享的资源(如全局配置、登录状态)
- 昂贵的初始化操作(如启动服务器、连接外部服务)
- 测试之间的状态共享
示例:
import pytest
@pytest.fixture(scope="session")
def test_server():
"""启动测试服务器(整个会话只需要启动一次)"""
print("启动测试服务器...")
server = {"url": "http://localhost:8080", "status": "running"}
yield server
print("关闭测试服务器...")
server["status"] = "stopped"
# 在多个文件中都可以使用 test_server
3.6 作用域对比总结
| 作用域 | 执行次数 | 使用场景 | 示例 |
|---|---|---|---|
| function | 每个测试函数一次 | 每个测试需要独立数据 | 临时文件、独立数据库连接 |
| class | 每个测试类一次 | 类中测试共享资源 | 类的初始化、共享状态 |
| module | 每个测试文件一次 | 文件内测试共享资源 | 配置文件、模块级连接 |
| package | 每个包一次 | 包内测试共享资源 | 包级配置 |
| session | 整个会话一次 | 全局共享资源 | 登录状态、全局服务器 |
3.7 作用域的实际应用示例
import pytest
import time
# function 级别:每个测试都需要独立的计时器
@pytest.fixture(scope="function")
def timer():
start = time.time()
yield start
end = time.time()
print(f"测试耗时:{end - start:.2f}秒")
# class 级别:整个类共享一个用户
@pytest.fixture(scope="class")
def test_user():
print("创建测试用户")
user = {"id": 1, "name": "测试用户"}
yield user
print("删除测试用户")
# module 级别:整个模块共享数据库连接
@pytest.fixture(scope="module")
def db_connection():
print("连接数据库")
db = {"connected": True}
yield db
print("断开数据库")
# session 级别:整个会话共享登录状态
@pytest.fixture(scope="session")
def auth_token():
print("获取认证令牌")
token = "abc123xyz"
yield token
print("清除认证令牌")
class TestUserOperations:
def test_create_user(self, timer, test_user, db_connection, auth_token):
# 所有 fixture 都可以使用
assert test_user["name"] == "测试用户"
assert db_connection["connected"] is True
assert auth_token == "abc123xyz"
def test_update_user(self, timer, test_user, db_connection, auth_token):
# test_user 是共享的(class级别)
# db_connection 是共享的(module级别)
# auth_token 是共享的(session级别)
# timer 是新的(function级别)
test_user["name"] = "更新后的用户"
assert test_user["name"] == "更新后的用户"
4. Fixture 的参数化
4.1 什么是 Fixture 参数化
Fixture 参数化允许你为同一个 fixture 提供多组不同的数据,pytest 会为每组数据运行一次测试。
4.2 基本语法
使用 @pytest.fixture(params=...) 来参数化 fixture:
import pytest
@pytest.fixture(params=["chrome", "firefox", "edge"])
def browser(request):
"""参数化的浏览器 fixture"""
browser_name = request.param
print(f"启动浏览器:{browser_name}")
yield browser_name
print(f"关闭浏览器:{browser_name}")
def test_open_page(browser):
"""这个测试会运行3次,每次使用不同的浏览器"""
print(f"在 {browser} 中打开页面")
assert browser in ["chrome", "firefox", "edge"]
执行结果:
启动浏览器:chrome
在 chrome 中打开页面
关闭浏览器:chrome
启动浏览器:firefox
在 firefox 中打开页面
关闭浏览器:firefox
启动浏览器:edge
在 edge 中打开页面
关闭浏览器:edge
4.3 request.param 详解
request 是 pytest 提供的一个内置 fixture,它包含了当前测试请求的信息。request.param 就是当前参数的值。
import pytest
@pytest.fixture(params=[1, 2, 3, 4, 5])
def number(request):
"""参数化的数字 fixture"""
num = request.param
print(f"当前参数值:{num}")
return num
def test_square(number):
"""测试平方"""
result = number ** 2
print(f"{number} 的平方是 {result}")
assert result == number * number
4.4 参数化的实际应用
4.4.1 测试不同的数据库
import pytest
@pytest.fixture(params=["mysql", "postgresql", "sqlite"])
def database(request):
"""参数化的数据库 fixture"""
db_type = request.param
print(f"连接 {db_type} 数据库")
# 模拟数据库连接
db = {
"type": db_type,
"connected": True
}
yield db
print(f"断开 {db_type} 数据库连接")
db["connected"] = False
def test_query(database):
"""测试查询操作"""
assert database["connected"] is True
print(f"在 {database['type']} 中执行查询")
def test_insert(database):
"""测试插入操作"""
assert database["connected"] is True
print(f"在 {database['type']} 中执行插入")
说明:每个测试函数都会在三种数据库上各运行一次,总共运行 6 次(2个测试 × 3个数据库)。
4.4.2 测试不同的用户角色
import pytest
@pytest.fixture(params=[
{"role": "admin", "permissions": ["read", "write", "delete"]},
{"role": "user", "permissions": ["read"]},
{"role": "guest", "permissions": []}
])
def user_role(request):
"""参数化的用户角色 fixture"""
role_data = request.param
print(f"创建 {role_data['role']} 用户")
yield role_data
print(f"清理 {role_data['role']} 用户")
def test_read_permission(user_role):
"""测试读取权限"""
if "read" in user_role["permissions"]:
assert True
print(f"{user_role['role']} 有读取权限")
else:
assert False, f"{user_role['role']} 没有读取权限"
def test_write_permission(user_role):
"""测试写入权限"""
has_write = "write" in user_role["permissions"]
if user_role["role"] == "admin":
assert has_write is True
else:
assert has_write is False
4.4.3 组合参数化
你可以让测试函数使用多个参数化的 fixture,pytest 会生成所有可能的组合:
import pytest
@pytest.fixture(params=["GET", "POST", "PUT"])
def http_method(request):
"""HTTP 方法参数化"""
return request.param
@pytest.fixture(params=["/api/users", "/api/products", "/api/orders"])
def api_endpoint(request):
"""API 端点参数化"""
return request.param
def test_api_request(http_method, api_endpoint):
"""测试 API 请求(会运行 3×3=9 次)"""
print(f"发送 {http_method} 请求到 {api_endpoint}")
assert http_method in ["GET", "POST", "PUT"]
assert api_endpoint.startswith("/api/")
说明:这个测试会运行 9 次(3种方法 × 3个端点)。
5. Fixture 的依赖和嵌套
5.1 Fixture 依赖其他 Fixture
Fixture 可以依赖其他 fixture,只需要在 fixture 函数的参数中声明即可。
5.1.1 基本依赖
import pytest
@pytest.fixture
def database_url():
"""提供数据库URL"""
return "mysql://localhost:3306/testdb"
@pytest.fixture
def database_connection(database_url):
"""依赖 database_url fixture"""
print(f"使用 {database_url} 连接数据库")
connection = {"url": database_url, "connected": True}
yield connection
print("关闭数据库连接")
connection["connected"] = False
def test_query(database_connection):
"""使用依赖链"""
assert database_connection["connected"] is True
assert "mysql://" in database_connection["url"]
执行流程:
- pytest 发现
test_query需要database_connection - pytest 发现
database_connection需要database_url - 先执行
database_url()fixture - 将结果传递给
database_connection()fixture - 将结果传递给
test_query()测试函数
5.1.2 多层依赖
import pytest
@pytest.fixture
def config():
"""配置 fixture"""
return {"host": "localhost", "port": 8080}
@pytest.fixture
def server_url(config):
"""服务器URL fixture(依赖 config)"""
return f"http://{config['host']}:{config['port']}"
@pytest.fixture
def api_client(server_url):
"""API客户端 fixture(依赖 server_url)"""
print(f"创建API客户端,连接到 {server_url}")
client = {"url": server_url, "authenticated": True}
yield client
print("关闭API客户端")
client["authenticated"] = False
def test_api_call(api_client):
"""测试API调用"""
assert api_client["authenticated"] is True
assert "http://" in api_client["url"]
依赖链:config → server_url → api_client → test_api_call
5.2 使用 autouse 自动使用 Fixture
5.2.1 什么是 autouse
autouse=True 表示 fixture 会自动执行,不需要在测试函数中声明。
import pytest
@pytest.fixture(autouse=True)
def setup_and_teardown():
"""自动执行的 fixture"""
print("【自动】测试开始前的准备")
yield
print("【自动】测试结束后的清理")
def test_one():
"""不需要声明 fixture,但会自动执行"""
print("执行测试1")
assert True
def test_two():
"""不需要声明 fixture,但会自动执行"""
print("执行测试2")
assert True
执行结果:
【自动】测试开始前的准备
执行测试1
【自动】测试结束后的清理
【自动】测试开始前的准备
执行测试2
【自动】测试结束后的清理
5.2.2 autouse 的使用场景
场景1:全局日志设置
import pytest
import logging
@pytest.fixture(autouse=True)
def setup_logging():
"""自动设置日志"""
logging.basicConfig(level=logging.INFO)
print("日志系统已初始化")
yield
print("日志系统已关闭")
def test_with_logging():
"""所有测试都会自动有日志功能"""
logging.info("这是一条日志信息")
assert True
场景2:测试环境检查
import pytest
import os
@pytest.fixture(autouse=True)
def check_test_environment():
"""自动检查测试环境"""
assert os.getenv("TEST_ENV") is not None, "测试环境未设置"
print("测试环境检查通过")
yield
print("测试环境检查完成")
def test_something():
"""自动进行环境检查"""
assert True
场景3:清理临时文件
import pytest
import os
import tempfile
@pytest.fixture(autouse=True, scope="function")
def cleanup_temp_files():
"""自动清理临时文件"""
yield
# 测试结束后清理
temp_dir = tempfile.gettempdir()
for file in os.listdir(temp_dir):
if file.startswith("test_"):
file_path = os.path.join(temp_dir, file)
if os.path.isfile(file_path):
os.remove(file_path)
print(f"删除临时文件:{file_path}")
5.3 Fixture 的嵌套使用
5.3.1 基本嵌套
import pytest
@pytest.fixture
def outer_fixture():
print("外层 fixture 开始")
outer_data = "外层数据"
yield outer_data
print("外层 fixture 结束")
@pytest.fixture
def inner_fixture(outer_fixture):
print("内层 fixture 开始")
inner_data = f"{outer_fixture} + 内层数据"
yield inner_data
print("内层 fixture 结束")
def test_nested(inner_fixture):
print(f"测试使用:{inner_fixture}")
assert "外层数据" in inner_fixture
assert "内层数据" in inner_fixture
执行结果:
外层 fixture 开始
内层 fixture 开始
测试使用:外层数据 + 内层数据
内层 fixture 结束
外层 fixture 结束
5.3.2 复杂嵌套示例
import pytest
@pytest.fixture
def base_config():
"""基础配置"""
return {"app_name": "MyApp", "version": "1.0"}
@pytest.fixture
def database_config(base_config):
"""数据库配置(依赖基础配置)"""
config = base_config.copy()
config["database"] = {"host": "localhost", "port": 3306}
return config
@pytest.fixture
def api_config(database_config):
"""API配置(依赖数据库配置)"""
config = database_config.copy()
config["api"] = {"base_url": "http://api.example.com"}
return config
@pytest.fixture
def full_app(api_config):
"""完整应用(依赖API配置)"""
app = {
"config": api_config,
"initialized": True
}
yield app
app["initialized"] = False
def test_app(full_app):
"""测试应用"""
assert full_app["initialized"] is True
assert full_app["config"]["app_name"] == "MyApp"
assert "database" in full_app["config"]
assert "api" in full_app["config"]
6. conftest.py 文件
6.1 什么是 conftest.py
conftest.py 是 pytest 的一个特殊文件,用于存放共享的 fixture。pytest 会自动发现这个文件并加载其中的 fixture。
6.2 conftest.py 的作用
- 共享 fixture:多个测试文件可以共享同一个 fixture
- 组织代码:将 fixture 从测试文件中分离出来
- 作用域控制:fixture 的作用域可以覆盖整个目录及其子目录
6.3 基本使用
6.3.1 创建 conftest.py
目录结构:
project/
├── conftest.py # 共享的 fixture
├── test_login.py # 测试文件1
└── test_logout.py # 测试文件2
conftest.py:
import pytest
@pytest.fixture
def user_data():
"""共享的用户数据 fixture"""
return {"username": "test_user", "password": "test123"}
@pytest.fixture
def login_session(user_data):
"""共享的登录会话 fixture"""
print(f"用户 {user_data['username']} 登录")
session = {"user": user_data, "logged_in": True}
yield session
print(f"用户 {user_data['username']} 登出")
session["logged_in"] = False
test_login.py:
def test_login(login_session):
"""测试登录"""
assert login_session["logged_in"] is True
assert login_session["user"]["username"] == "test_user"
test_logout.py:
def test_logout(login_session):
"""测试登出"""
assert login_session["logged_in"] is True
# 执行登出操作
login_session["logged_in"] = False
assert login_session["logged_in"] is False
说明:两个测试文件都可以直接使用 login_session fixture,不需要导入。
6.3.2 conftest.py 的作用域
conftest.py 中的 fixture 可以被同一目录及子目录中的测试文件使用。
目录结构:
project/
├── conftest.py # 根级 conftest
├── test_a.py
├── api/
│ ├── conftest.py # API 子目录的 conftest
│ ├── test_api1.py
│ └── test_api2.py
└── ui/
├── conftest.py # UI 子目录的 conftest
└── test_ui.py
根级 conftest.py:
import pytest
@pytest.fixture(scope="session")
def global_config():
"""全局配置,所有测试都可以使用"""
return {"app_name": "MyApp"}
api/conftest.py:
import pytest
@pytest.fixture
def api_client():
"""API客户端,只有 api 目录下的测试可以使用"""
return {"base_url": "http://api.example.com"}
test_a.py(在根目录):
def test_global(global_config):
"""可以使用根级 conftest 的 fixture"""
assert global_config["app_name"] == "MyApp"
def test_api(api_client):
"""不能使用 api 子目录的 fixture,会报错"""
pass # 这会失败
api/test_api1.py:
def test_with_global(global_config):
"""可以使用根级 conftest 的 fixture"""
assert global_config["app_name"] == "MyApp"
def test_with_api(api_client):
"""可以使用 api 子目录的 conftest 的 fixture"""
assert "api.example.com" in api_client["base_url"]
6.4 conftest.py 的实际应用
6.4.1 数据库连接
conftest.py:
import pytest
@pytest.fixture(scope="session")
def database():
"""全局数据库连接"""
print("连接数据库")
db = {
"host": "localhost",
"port": 3306,
"connected": True
}
yield db
print("断开数据库连接")
db["connected"] = False
@pytest.fixture
def db_session(database):
"""数据库会话(每个测试一个)"""
print("开始数据库事务")
yield database
print("回滚数据库事务")
test_users.py:
def test_create_user(db_session):
"""创建用户"""
assert db_session["connected"] is True
# 执行创建操作
def test_delete_user(db_session):
"""删除用户"""
assert db_session["connected"] is True
# 执行删除操作
6.4.2 API 测试配置
conftest.py:
import pytest
@pytest.fixture(scope="session")
def api_base_url():
"""API 基础URL"""
return "https://api.example.com"
@pytest.fixture(scope="session")
def auth_token():
"""认证令牌"""
# 实际项目中,这里会调用登录接口获取token
return "test_token_12345"
@pytest.fixture
def api_headers(auth_token):
"""API 请求头"""
return {
"Authorization": f"Bearer {auth_token}",
"Content-Type": "application/json"
}
test_api.py:
import requests
def test_get_users(api_base_url, api_headers):
"""获取用户列表"""
response = requests.get(
f"{api_base_url}/users",
headers=api_headers
)
assert response.status_code == 200
def test_create_user(api_base_url, api_headers):
"""创建用户"""
data = {"name": "新用户", "email": "new@example.com"}
response = requests.post(
f"{api_base_url}/users",
json=data,
headers=api_headers
)
assert response.status_code == 201
7. Pytest 内置 Fixture
7.1 什么是内置 Fixture
Pytest 提供了一些内置的 fixture,可以直接使用,无需定义。这些 fixture 提供了测试中常用的功能。
7.2 常用的内置 Fixture
7.2.1 tmp_path 和 tmp_path_factory
tmp_path:为每个测试函数提供一个临时目录路径(Path 对象)。
def test_write_file(tmp_path):
"""使用临时目录"""
# tmp_path 是一个 Path 对象
file_path = tmp_path / "test.txt"
file_path.write_text("测试内容", encoding="utf-8")
# 读取文件
content = file_path.read_text(encoding="utf-8")
assert content == "测试内容"
# 测试结束后,临时目录会自动删除
tmp_path_factory:用于创建临时目录,通常用于 session 或 module 级别的 fixture。
@pytest.fixture(scope="module")
def shared_temp_dir(tmp_path_factory):
"""模块级别的临时目录"""
temp_dir = tmp_path_factory.mktemp("shared_data")
return temp_dir
def test_one(shared_temp_dir):
file1 = shared_temp_dir / "file1.txt"
file1.write_text("文件1")
assert file1.exists()
def test_two(shared_temp_dir):
file2 = shared_temp_dir / "file2.txt"
file2.write_text("文件2")
assert file2.exists()
# file1 仍然存在,因为共享同一个临时目录
7.2.2 tmpdir 和 tmpdir_factory
tmpdir:与 tmp_path 类似,但返回的是 py.path.local 对象(旧版 API,推荐使用 tmp_path)。
def test_old_way(tmpdir):
"""旧版临时目录(不推荐,但可能遇到)"""
file_path = tmpdir.join("test.txt")
file_path.write("测试内容")
assert file_path.read() == "测试内容"
7.2.3 capsys 和 capsysbinary
capsys:捕获标准输出和标准错误。
def test_output(capsys):
"""测试输出"""
print("这是标准输出")
print("这是错误输出", file=sys.stderr)
# 捕获输出
captured = capsys.readouterr()
assert "这是标准输出" in captured.out
assert "这是错误输出" in captured.err
capsysbinary:捕获二进制输出。
def test_binary_output(capsysbinary):
"""测试二进制输出"""
sys.stdout.buffer.write(b"binary data")
captured = capsysbinary.readouterr()
assert b"binary data" in captured.out
7.2.4 caplog
caplog:捕获日志输出。
import logging
def test_logging(caplog):
"""测试日志"""
logger = logging.getLogger("my_logger")
logger.setLevel(logging.INFO)
logger.info("这是一条信息日志")
logger.warning("这是一条警告日志")
logger.error("这是一条错误日志")
# 检查日志
assert "这是一条信息日志" in caplog.text
assert "这是一条警告日志" in caplog.text
assert "这是一条错误日志" in caplog.text
# 检查特定级别的日志
assert len(caplog.records) == 3
assert caplog.records[0].levelname == "INFO"
7.2.5 monkeypatch
monkeypatch:临时修改对象、函数或环境变量。
def test_monkeypatch_function(monkeypatch):
"""修改函数"""
def mock_get_data():
return "模拟数据"
# 临时替换函数
monkeypatch.setattr("module.get_data", mock_get_data)
# 现在调用 get_data() 会返回 "模拟数据"
result = module.get_data()
assert result == "模拟数据"
def test_monkeypatch_env(monkeypatch):
"""修改环境变量"""
monkeypatch.setenv("TEST_MODE", "true")
assert os.getenv("TEST_MODE") == "true"
# 测试结束后,环境变量会自动恢复
def test_monkeypatch_attribute(monkeypatch):
"""修改对象属性"""
obj = SomeClass()
monkeypatch.setattr(obj, "attribute", "新值")
assert obj.attribute == "新值"
7.2.6 request
request:获取当前测试请求的信息。
def test_request_info(request):
"""获取测试信息"""
# 测试函数名称
print(f"测试函数:{request.function.__name__}")
# 测试文件路径
print(f"测试文件:{request.fspath}")
# 测试类(如果有)
if request.cls:
print(f"测试类:{request.cls.__name__}")
# 所有 fixture 名称
print(f"使用的 fixture:{request.fixturenames}")
# 获取 fixture 值
if "my_fixture" in request.fixturenames:
value = request.getfixturevalue("my_fixture")
print(f"Fixture 值:{value}")
7.3 内置 Fixture 完整示例
import pytest
import sys
import logging
import os
def test_with_tmp_path(tmp_path):
"""使用临时路径"""
file = tmp_path / "test.txt"
file.write_text("内容")
assert file.read_text() == "内容"
def test_with_capsys(capsys):
"""捕获输出"""
print("输出内容")
captured = capsys.readouterr()
assert "输出内容" in captured.out
def test_with_caplog(caplog):
"""捕获日志"""
logging.info("日志信息")
assert "日志信息" in caplog.text
def test_with_monkeypatch(monkeypatch):
"""修改环境"""
monkeypatch.setenv("TEST_VAR", "test_value")
assert os.getenv("TEST_VAR") == "test_value"
def test_with_request(request):
"""获取测试信息"""
assert request.function.__name__ == "test_with_request"
8. Fixture 的高级用法
8.1 使用 pytest.fixture 的参数
8.1.1 常用参数总结
@pytest.fixture(
scope="function", # 作用域
params=[1, 2, 3], # 参数化
autouse=False, # 是否自动使用
ids=None, # 参数化的ID
name=None # fixture 的名称
)
def my_fixture(request):
pass
8.1.2 ids 参数:自定义参数化ID
import pytest
@pytest.fixture(
params=["chrome", "firefox", "edge"],
ids=["Chrome浏览器", "Firefox浏览器", "Edge浏览器"]
)
def browser(request):
return request.param
def test_browser(browser):
assert browser in ["chrome", "firefox", "edge"]
运行时会显示:
test_browser[Chrome浏览器] PASSED
test_browser[Firefox浏览器] PASSED
test_browser[Edge浏览器] PASSED
8.1.3 name 参数:重命名 Fixture
import pytest
@pytest.fixture(name="db")
def database_connection():
"""实际函数名是 database_connection,但使用时叫 db"""
return {"connected": True}
def test_example(db): # 使用短名称 db
assert db["connected"] is True
8.2 条件 Fixture
8.2.1 根据条件决定是否使用 Fixture
import pytest
@pytest.fixture
def conditional_fixture(request):
"""根据标记决定行为"""
if "slow" in request.keywords:
# 如果是慢速测试,使用真实数据库
return connect_real_database()
else:
# 否则使用模拟数据库
return create_mock_database()
@pytest.mark.slow
def test_slow_operation(conditional_fixture):
"""慢速测试使用真实数据库"""
assert conditional_fixture["type"] == "real"
def test_fast_operation(conditional_fixture):
"""快速测试使用模拟数据库"""
assert conditional_fixture["type"] == "mock"
8.3 Fixture 的最终化器(Finalizer)
除了使用 yield,还可以使用 request.addfinalizer() 来注册清理函数:
import pytest
@pytest.fixture
def resource(request):
"""使用 finalizer 的 fixture"""
print("初始化资源")
resource = {"data": "重要数据"}
def cleanup():
print("清理资源")
resource.clear()
# 注册清理函数
request.addfinalizer(cleanup)
return resource
def test_resource(resource):
assert resource["data"] == "重要数据"
# 测试结束后会自动调用 cleanup
yield 与 finalizer 的区别:
yield:更简洁,推荐使用finalizer:可以注册多个清理函数,更灵活
@pytest.fixture
def multi_cleanup(request):
"""多个清理函数"""
resource1 = create_resource1()
resource2 = create_resource2()
def cleanup1():
cleanup_resource1(resource1)
def cleanup2():
cleanup_resource2(resource2)
request.addfinalizer(cleanup1)
request.addfinalizer(cleanup2)
return resource1, resource2
8.4 Fixture 工厂模式
8.4.1 什么是 Fixture 工厂
Fixture 工厂是一个返回 fixture 函数的函数,用于创建多个类似的 fixture。
import pytest
def make_user_fixture(role):
"""Fixture 工厂:创建不同角色的用户 fixture"""
@pytest.fixture
def user():
return {"username": "test_user", "role": role}
return user
# 使用工厂创建多个 fixture
admin_user = make_user_fixture("admin")
regular_user = make_user_fixture("user")
guest_user = make_user_fixture("guest")
def test_admin(admin_user):
assert admin_user["role"] == "admin"
def test_user(regular_user):
assert regular_user["role"] == "user"
8.4.2 更实用的 Fixture 工厂
import pytest
@pytest.fixture
def user_factory():
"""用户工厂 fixture"""
def _create_user(role="user", username=None):
"""创建用户的内部函数"""
if username is None:
username = f"test_{role}"
return {
"username": username,
"role": role,
"created_at": "2024-01-01"
}
return _create_user
def test_create_admin(user_factory):
"""使用工厂创建管理员"""
admin = user_factory(role="admin", username="admin_user")
assert admin["role"] == "admin"
assert admin["username"] == "admin_user"
def test_create_regular_user(user_factory):
"""使用工厂创建普通用户"""
user = user_factory() # 使用默认值
assert user["role"] == "user"
assert user["username"] == "test_user"
9. 实际应用示例
9.1 Web 测试示例
import pytest
from selenium import webdriver
@pytest.fixture(scope="session")
def browser():
"""浏览器 fixture(整个会话只启动一次)"""
print("启动浏览器")
driver = webdriver.Chrome()
driver.maximize_window()
yield driver
print("关闭浏览器")
driver.quit()
@pytest.fixture
def login(browser):
"""登录 fixture"""
browser.get("http://example.com/login")
browser.find_element_by_id("username").send_keys("test_user")
browser.find_element_by_id("password").send_keys("test_pass")
browser.find_element_by_id("login_btn").click()
yield browser
# 登出
browser.find_element_by_id("logout_btn").click()
def test_homepage(browser):
"""测试首页"""
browser.get("http://example.com")
assert "首页" in browser.title
def test_user_profile(login):
"""测试用户资料(需要登录)"""
login.get("http://example.com/profile")
assert "个人资料" in login.page_source
9.2 API 测试示例
import pytest
import requests
@pytest.fixture(scope="session")
def api_base_url():
"""API 基础URL"""
return "https://api.example.com"
@pytest.fixture(scope="session")
def auth_token(api_base_url):
"""获取认证令牌"""
response = requests.post(
f"{api_base_url}/auth/login",
json={"username": "test", "password": "test123"}
)
token = response.json()["token"]
yield token
# 登出
requests.post(f"{api_base_url}/auth/logout", headers={"Authorization": f"Bearer {token}"})
@pytest.fixture
def api_headers(auth_token):
"""API 请求头"""
return {
"Authorization": f"Bearer {auth_token}",
"Content-Type": "application/json"
}
def test_get_users(api_base_url, api_headers):
"""获取用户列表"""
response = requests.get(f"{api_base_url}/users", headers=api_headers)
assert response.status_code == 200
assert isinstance(response.json(), list)
def test_create_user(api_base_url, api_headers):
"""创建用户"""
data = {"name": "新用户", "email": "new@example.com"}
response = requests.post(f"{api_base_url}/users", json=data, headers=api_headers)
assert response.status_code == 200