创建时间: 2026-09-01最后更新: 2026-09-01

1. 测试库

数据库测试应该连接独立测试数据库,绝不能把开发库或生产库拿来清空和回滚。

在 compose.yaml 中增加测试服务:

compose.yaml
01
services:
02
postgres:
03
image: postgres:18
04
environment:
05
POSTGRES_DB: task_app
06
POSTGRES_USER: task_app
07
POSTGRES_PASSWORD: task_app_dev_password
08
ports:
09
- "5432:5432"
10
volumes:
11
- postgres_data:/var/lib/postgresql
12
13
postgres_test:
14
image: postgres:18
15
environment:
16
POSTGRES_DB: task_app_test
17
POSTGRES_USER: task_app_test
18
POSTGRES_PASSWORD: task_app_test_password
19
ports:
20
- "5433:5432"
21
profiles: ["test"]
22
23
volumes:
24
postgres_data:

启动测试数据库:

start-test-database.bash
1
docker compose --profile test up -d postgres_test

测试环境使用不同端口、数据库名和账号:

.env.test
1
DATABASE_URL=postgresql+psycopg://task_app_test:task_app_test_password@localhost:5433/task_app_test

测试启动前先执行全部迁移:

migrate-test-database.bash
1
env -u DATABASE_URL uv run --env-file .env.test alembic upgrade head

uv 会优先使用当前 Shell 已经存在的环境变量,因此命令先移除可能残留的 DATABASE_URL,再从 .env.test 加载测试连接。这样测试验证的是部署会使用的真实迁移结构,而不是另外维护一套 create_all() 测试结构。

测试工具

安装测试依赖:

install-test-tools.bash
1
uv add --group test pytest pytest-asyncio httpx asgi-lifespan

在 pyproject.toml 中配置异步测试模式:

pyproject.toml
1
[tool.pytest.ini_options]
2
asyncio_mode = "auto"
3
asyncio_default_fixture_loop_scope = "function"
4
asyncio_default_test_loop_scope = "function"
5
testpaths = ["tests"]

asyncio_mode = "auto" 让 pytest 自动识别异步测试和异步 Fixture。这里又把测试与 Fixture 的事件循环范围明确设为 function,每个用例都会获得独立事件循环,测试之间不会意外共享异步任务。

创建测试 Engine 时必须再次确认 URL 指向测试库:

tests/database.py
01
from sqlalchemy.engine import make_url
02
from sqlalchemy.ext.asyncio import create_async_engine
03
from sqlalchemy.pool import NullPool
04
05
from app.core.settings import settings
06
07
08
url = make_url(settings.database_url)
09
if not (url.database or "").endswith("_test"):
10
raise RuntimeError("数据库测试只能连接名称以 _test 结尾的数据库")
11
12
test_engine = create_async_engine(
13
settings.database_url,
14
poolclass=NullPool,
15
)

测试 Engine 定义在模块级,会被不同用例的事件循环使用。NullPool 不复用驱动连接,可以避免某个用例创建的连接被下一个事件循环再次取出。另一种做法是让所有相关测试共享同一个事件循环,并在退出时释放 Engine;两种方式选择一种即可。

名称检查不是绝对安全证明,但可以阻止最常见的环境变量误配。CI 还应该使用专门的临时数据库账号和网络环境。运行测试时同样要显式加载测试环境变量:

run-database-tests.bash
1
env -u DATABASE_URL uv run --env-file .env.test pytest

环境变量必须在 Python 进程启动前加载。否则 settings 可能已经从默认 .env 创建并缓存,等测试代码开始执行时再修改环境变量已经太迟。

正在验证登录状态
请稍候,验证完成后将继续显示文章内容