用 pytest fixture 管理临时测试数据库
测试数据库最容易出问题的地方,通常不是 SQL 写错,而是多个测试共用同一份数据。一个用例刚插入记录,另一个用例就假设表是空的;单独运行能通过,按顺序一起运行却失败。把数据库的创建、初始化和清理交给 pytest fixture,可以让每个测试拥有独立环境。
下面以一个待办事项表为例。先在项目的 conftest.py 中创建临时数据库:
import sqlite3
import pytest
@pytest.fixture
def db(tmp_path):
connection = sqlite3.connect(tmp_path / "test.db")
connection.executescript(
"""
CREATE TABLE todo (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);
"""
)
try:
yield connection
finally:
connection.close()
tmp_path 是 pytest 提供的内置 fixture。每次调用都会得到一个独立目录,测试结束后由 pytest 清理,因此不需要手写删除文件的逻辑。db fixture 使用 yield 把连接交给测试;无论测试成功还是抛异常,finally 都会执行,连接不会残留。
测试直接使用这个 fixture:
def test_create_todo(db):
db.execute(
"INSERT INTO todo (title) VALUES (?)",
("整理发布清单",),
)
db.commit()
rows = db.execute(
"SELECT title FROM todo"
).fetchall()
assert rows == [("整理发布清单",)]
第二个测试查不到第一个测试的数据,因为两个测试拿到的是不同的临时数据库:
def test_todo_list_starts_empty(db):
rows = db.execute(
"SELECT title FROM todo"
).fetchall()
assert rows == []
当多个测试都需要一组基础数据时,可以再增加一个依赖 db 的 fixture,而不要在 db 中自动塞数据:
@pytest.fixture
def todo_with_item(db):
db.execute(
"INSERT INTO todo (title) VALUES (?)",
("准备周报",),
)
db.commit()
return db
这样只有显式声明 todo_with_item 的测试才会获得预置记录。普通数据库测试仍然从空表开始,数据准备关系也更清楚。不要把这类逻辑设成 autouse=True,否则所有测试都会被隐式修改,排查问题时很难看出数据来自哪里。
这套结构也方便扩展。需要更接近真实数据库时,可以在同一个 db fixture 中替换连接方式,但测试用例仍只依赖 fixture 返回的连接接口。团队最好把 fixture 放在共享的 conftest.py 中,把业务专用数据放到对应测试目录,避免所有场景都堆进一个全局 fixture。
临时数据库只能解决测试隔离,不能代替业务事务设计。需要验证回滚、并发或锁等待时,仍应编写针对性用例。但对大多数增删改查测试,tmp_path 加 yield 已经足够简单:每个测试从确定的起点开始,结束后自动释放资源,失败也不会污染下一次运行。
把测试从共享数据库中拆出来之后,执行顺序不再影响结果,开发者也能放心单独重跑失败的用例。这是 fixture 带来的直接价值。
- 点赞
- 收藏
- 关注作者
评论(0)