FastAPI使用TestClient测试时,模拟环境变量无效导致应用加载失败的问题解决
我一眼就看出问题出在哪了——你的password_encryptor是在模块导入阶段就初始化的,而你的pytest fixture是在测试用例运行前才去mock环境变量,这完全赶不上趟!
当你在测试文件里写from src.app.main import app的时候,main.py里的所有顶层代码(包括password_encryptor = PasswordEncryptor(...)这一行)就已经执行了,这时候你的mock还没生效,os.environ.get自然返回None,导致后续报错。
下面给你两种靠谱的解决办法,选哪个看你的需求:
方法一:把初始化逻辑放到FastAPI的启动事件中(推荐)
这是最符合FastAPI设计理念的做法,把需要依赖环境变量的初始化逻辑延迟到app启动时执行,这样mock的环境变量就能生效了:
修改你的main.py:
import os from fastapi import FastAPI from cryptography.fernet import Fernet from src.app.auth import PasswordEncryptor app = FastAPI(docs_url="/jopapi/documentation", redoc_url=None) password_encryptor = None # 先占位 @app.on_event("startup") async def startup_event(): global password_encryptor password_encryptor = PasswordEncryptor( os.environ.get("JUPYTERHUB_COOKIE_SECRET"), fernet=Fernet ) ...
这样,只有当TestClient启动app的时候,才会执行startup_event,这时候你的fixture里的mock已经设置好了环境变量,就能正常获取到值了。
方法二:先mock环境变量,再导入app
如果你不想修改业务代码,那可以调整测试文件的执行顺序,确保在导入app之前就把环境变量mock好:
修改你的测试文件:
import os from unittest import mock import pytest from fastapi.testclient import TestClient # 先mock环境变量,再导入app with mock.patch.dict(os.environ, {"JUPYTERHUB_COOKIE_SECRET": "471bAcmHjbIdu3KLWphYpgXSW1HNC8q7"}, clear=True): from src.app.main import app @pytest.fixture(name="client") def client_fixture(): client = TestClient(app) yield client def test_generic(client: TestClient): response = client.get("/jobapi/generic") assert response.status_code == 200
或者用autouse fixture来提前mock,确保在所有测试前生效:
import os from unittest import mock import pytest from fastapi.testclient import TestClient @pytest.fixture(autouse=True, scope="session") def mock_env_vars(): with mock.patch.dict(os.environ, {"JUPYTERHUB_COOKIE_SECRET": "471bAcmHjbIdu3KLWphYpgXSW1HNC8q7"}, clear=True): yield # 现在可以安全导入app了 from src.app.main import app @pytest.fixture(name="client") def client_fixture(): client = TestClient(app) yield client def test_generic(client: TestClient): response = client.get("/jobapi/generic") assert response.status_code == 200
为什么之前的方法不行?
再啰嗦一句加深理解:Python模块是导入即执行的,当你导入main.py时,顶层的代码会立刻运行。你的fixture是在测试用例的setup阶段才执行mock,这时候模块已经导入完了,初始化代码早就跑完了,mock自然无效。
选上面任意一种方法,你的测试应该就能正常通过了!
内容的提问来源于stack exchange,提问作者Data Mastery

