使用Pytest时Dependency_overrides不生效的问题排查与解决
问题与解决方案
问题情况
运行FastAPI测试时,db.py中执行settings = get_settings()触发报错:
1 validation error for Settings, DATABASE_URL, Field required [type=missing, input_value={}, input_type=dict]
已在conftest.py中重写get_settings函数,但仍出现该错误——因为初始化数据库时会从环境变量读取DATABASE_URL,但测试环境未配置该变量。
原代码文件
main.py
# main.py @app.on_event("startup") def on_startup(): logging.info("Creating database tables") SQLModel.metadata.create_all(engine) @app.get("/ping")def ping(settings = Depends(get_settings)): return settings.DATABASE_URL
db.py
# db.py from sqlmodel import create_engine, Session from app.config import get_settings settings = get_settings() # <- 此处报错 engine = create_engine(settings.DATABASE_URL)
config.py
# config.py from functools import lru_cache from pydantic_settings import BaseSettings class Settings(BaseSettings): DATABASE_URL: str # 在实际容器的环境中设置 @lru_cache def get_settings(): return Settings()
conftest.py
# conftest.py import pytest from fastapi.testclient import TestClient from app.config import Settings from app.main import app def get_settings_override(): DATABASE_TEST_URL = "mssql+pyodbc://sa:Password123@db:1433/master?driver=ODBC+Driver+18+for+SQL+Server&Encrypt=No" return Settings(DATABASE_URL=DATABASE_TEST_URL) @pytest.fixture() def test_app(): with TestClient(app) as test_client: yield test_client
test_ping.py
# test_ping.py def test_ping(test_app): response = test_app.get("/ping") assert response.status_code == 200 assert response.json() == "pong!"
问题原因
- 模块加载时机问题:
db.py在模块被导入时就直接执行settings = get_settings(),此时pytest的fixture还未初始化,依赖覆盖逻辑还没生效,调用的是原始get_settings函数——它会从环境变量读取配置,而测试环境没设DATABASE_URL,所以报错。 - 未注册依赖覆盖:
conftest.py里只定义了get_settings_override,但没有把它注册到FastAPI的依赖覆盖列表中,就算fixture运行了,也不会替换原来的配置函数。
修复方案
1. 延迟数据库引擎初始化
修改db.py,不在模块级别直接初始化settings和engine,改用带缓存的函数获取,确保只有在实际使用时才调用get_settings():
# db.py from sqlmodel import create_engine, Session from app.config import get_settings from functools import lru_cache @lru_cache def get_engine(): settings = get_settings() return create_engine(settings.DATABASE_URL) def get_session(): engine = get_engine() with Session(engine) as session: yield session
2. 更新main.py的启动逻辑
在startup事件中调用get_engine()获取引擎,避免直接引用模块级别的engine:
# main.py import logging from fastapi import FastAPI, Depends from sqlmodel import SQLModel from app.db import get_engine from app.config import get_settings app = FastAPI() @app.on_event("startup") def on_startup(): logging.info("Creating database tables") engine = get_engine() SQLModel.metadata.create_all(engine) @app.get("/ping") def ping(settings = Depends(get_settings)): return settings.DATABASE_URL
3. 注册依赖覆盖到conftest.py
在test_app fixture中添加依赖覆盖的注册,确保测试时使用重写后的配置:
# conftest.py import pytest from fastapi.testclient import TestClient from app.config import Settings, get_settings from app.main import app def get_settings_override(): DATABASE_TEST_URL = "mssql+pyodbc://sa:Password123@db:1433/master?driver=ODBC+Driver+18+for+SQL+Server&Encrypt=No" return Settings(DATABASE_URL=DATABASE_TEST_URL) @pytest.fixture() def test_app(): # 注册依赖覆盖 app.dependency_overrides[get_settings] = get_settings_override with TestClient(app) as test_client: yield test_client # 测试结束后清除覆盖,避免影响其他测试用例 app.dependency_overrides.clear()
4. 修正测试断言
原测试断言的"pong!"与接口实际返回的数据库URL不符,修改为测试用的URL:
# test_ping.py def test_ping(test_app): response = test_app.get("/ping") assert response.status_code == 200 assert response.json() == "mssql+pyodbc://sa:Password123@db:1433/master?driver=ODBC+Driver+18+for+SQL+Server&Encrypt=No"
内容的提问来源于stack exchange,提问作者RogerKint
相关产品推荐
相关产品推荐

