千锋教育-做有情怀、有良心、有品质的职业教育机构

传统测试框架(比如 unittest)用 setUp 和 tearDown 管理前置和后置操作。简单场景够用,复杂场景就暴露短板:无法组合、无法参数化、无法跨文件共享。
Pytest Fixture 做了一件事:把测试依赖变成参数。
Before:每个测试文件里都写一遍数据库连接、登录逻辑、测试数据准备。
class TestUser:
def setup_method(self):
self.db = connect_db()
self.token = login("admin", "123456")
self.user_data = create_test_user()
def test_create_user(self):
# 测试逻辑
pass
def teardown_method(self):
self.db.close()
delete_test_user(self.user_data)
After:Fixture 声明依赖,测试函数只管用。
import pytest
@pytest.fixture
def db_connection():
conn = connect_db()
yield conn
conn.close()
@pytest.fixture
def admin_token(db_connection):
return login("admin", "123456")
@pytest.fixture
def test_user(db_connection):
user = create_test_user()
yield user
delete_test_user(user)
def test_create_user(test_user, admin_token):
# 测试逻辑:直接使用 test_user 和 admin_token
pass
> Fixture 不是 setup 的替代品,是依赖管理的思维方式。把"我要什么"变成"给我什么"。
---
Pytest Fixture 的依赖注入有多强?看三个关键词:自动发现、链式依赖、作用域控制。
自动发现:只要函数名出现在测试参数里,Pytest 自动调用对应 Fixture,不需要手动 import 或调用。
链式依赖:Fixture 可以依赖其他 Fixture,Pytest 自动构建依赖图。
@pytest.fixture
def api_client(admin_token):
return APIClient(base_url="https://api.example.com", token=admin_token)
@pytest.fixture
def user_service(api_client):
return UserService(api_client)
def test_get_user(user_service):
# Pytest 自动解析链路:
# test_get_user → user_service → api_client → admin_token
user = user_service.get_user(1)
assert user.name == "张三"
作用域控制:5个级别决定 Fixture 的生命周期。
| 作用域 | 说明 | 典型用途 |
|--------|------|---------|
| function | 每个测试函数执行一次(默认) | 创建临时测试数据 |
| class | 每个测试类执行一次 | 初始化浏览器实例 |
| module | 每个模块执行一次 | 数据库连接 |
| package | 每个包执行一次 | 共享配置 |
| session | 整个测试会话执行一次 | 启动测试服务器 |
@pytest.fixture(scope="session")
def test_server():
server = start_test_server()
yield server
server.stop()
@pytest.fixture(scope="function")
def fresh_db(test_server):
db = test_server.create_database()
yield db
db.drop()
> 作用域选错了,不是慢就是脏。函数级保纯净,会话级省资源。
---
真实项目里,几十个测试文件需要共享同一套 Fixture。答案就是 conftest.py。
规则:conftest.py 放在哪个目录,它里面的 Fixture 就对该目录下所有测试可见。不需要 import,Pytest 自动发现。
tests/
├── conftest.py # 全局 Fixture:数据库、登录、配置
├── api/
│ ├── conftest.py # API 测试专属 Fixture
│ ├── test_user.py
│ └── test_order.py
├── ui/
│ ├── conftest.py # UI 测试专属 Fixture
│ └── test_login.py
└── conftest.py # 根级,所有子目录都继承
Before:每个文件 copy 一份登录 Fixture,改密码要改50个文件。
After:只在根 conftest.py 写一次,所有文件自动可用。
# tests/conftest.py(全局)
@pytest.fixture
def base_url():
return "https://api.test.example.com"
@pytest.fixture
def auth_headers(base_url):
response = requests.post(f"{base_url}/auth/login", json={
"username": "test_admin",
"password": "test_pass"
})
token = response.json()["token"]
return {"Authorization": f"Bearer {token}"}
# tests/api/test_user.py(无需 import,直接用)
def test_list_users(auth_headers, base_url):
resp = requests.get(f"{base_url}/users", headers=auth_headers)
assert resp.status_code == 200
> conftest.py 是测试项目的公共基础设施。建好了,后面的路自己走。
---
依赖注入最具威力的用法之一:参数化 Fixture。一个 Fixture 定义多组数据,自动生成多个测试用例。
@pytest.fixture(params=[
{"username": "admin", "password": "admin123", "expected": True},
{"username": "guest", "password": "wrong", "expected": False},
{"username": "", "password": "", "expected": False},
])
def login_case(request):
return request.param
def test_login(login_case):
result = login(login_case["username"], login_case["password"])
assert result.success == login_case["expected"]
运行结果:自动生成3个独立测试用例,每个参数组合一个。
test_login[login_case0] ✅ PASSED
test_login[login_case1] ✅ PASSED
test_login[login_case2] ✅ PASSED
更进一步的:Fixture 组合 + 参数化。
@pytest.fixture(params=["chrome", "firefox", "edge"])
def browser(request):
if request.param == "chrome":
driver = webdriver.Chrome()
elif request.param == "firefox":
driver = webdriver.Firefox()
else:
driver = webdriver.Edge()
yield driver
driver.quit()
def test_search(browser):
browser.get("https://www.baidu.com")
browser.find_element(By.ID, "kw").send_keys("Pytest")
results = browser.find_elements(By.CSS_SELECTOR, ".result")
assert len(results) > 0
3个浏览器 × N个测试函数 = 3N 个测试用例,代码只写一份。
> 数据驱动不是多写几组数据,是一份数据跑出所有可能性。
---
来看一个真实场景的重构过程。
重构前:一个 conftest.py 文件里堆了30个 Fixture,互相依赖,改一个牵连十个。
# 问题代码:隐式依赖,职责不清
@pytest.fixture
def app():
db = create_db()
user = create_user(db)
token = login(db, user)
client = APIClient(token)
cache = RedisCache()
yield {"client": client, "db": db, "user": user, "token": token, "cache": cache}
cleanup_everything(db, user, cache)
重构后:拆分职责,显式声明依赖。
# 拆分为独立 Fixture,职责单一
@pytest.fixture(scope="session")
def database():
db = create_db()
yield db
db.close()
@pytest.fixture
def test_user(database):
user = create_user(database)
yield user
database.delete_user(user.id)
@pytest.fixture
def auth_token(database, test_user):
return generate_token(test_user)
@pytest.fixture
def api_client(auth_token):
return APIClient(token=auth_token)
# 测试用例只声明自己需要的
def test_user_profile(api_client, test_user):
resp = api_client.get(f"/users/{test_user.id}")
assert resp.status_code == 200
重构原则:
1. 单一职责:一个 Fixture 只做一件事
2. 显式依赖:需要什么就在参数里写什么
3. 合理作用域:能复用的用大作用域,有副作用的用小作用域
4. yield 清理:资源创建用 yield,后面紧跟清理逻辑
> 重构测试代码和重构产品代码一样重要。测试代码的质量,决定测试结果的可信度。
---
依赖注入不是炫技,是工程思维。Fixture 让你的测试代码从"能用"变成"好用",从"改不动"变成"随便改"。
记住三个关键词:声明依赖、控制作用域、单一职责。做到了,200个测试用例也能管得明明白白。
别让测试代码成为技术债。写得好的测试,是项目最好的文档。