我有一些现有的 FastAPI 测试不再通过,因为一些内部服务器逻辑已经更改,现在依赖于
datetime.now()
返回的值。
这是一个外部依赖项。通常,为了处理外部依赖项,我们会编写外部依赖项的模拟实现,并找到一种方法将其注入到正在测试的代码中。
我不确定这是否是像日期时间依赖这样简单的事情的最佳方法。可能是,也可能不是。
这是一些说明问题的 MWE 代码:
# lib_datetime.py
from datetime import datetime
from datetime import timezone
def now() -> datetime:
return datetime.now(timezone.utc)
# fastapi_webserver.py
from fastapi import FastAPI
from lib_datetime import now
from datetime import datetime
app = FastAPI()
@app.get('/datetime_now')
def datetime_now():
the_datetime_now = now()
the_datetime_now_str = the_datetime_now.strftime('%Y-%m-%d %H:%M:%S.%f %z')
return {
'datetime_now': the_datetime_now_str,
}
# test_fastapi_webserver.py
from fastapi.testclient import TestClient
from fastapi_webserver import app
from datetime import datetime
from datetime import timezone
client = TestClient(app)
def test_fastapi_webserver_datetime_now():
datetime_now = datetime(year=2024, month=7, day=20, tzinfo=timezone.utc)
datetime_now_str = datetime_now.strftime('%Y-%m-%d %H:%M:%S.%f %z')
response = client.get('/datetime_now')
print(response.json())
assert response.status_code == 200
assert response.json() == {
'datetime_now', datetime_now_str,
}
问题应该是相当明显的。返回的日期时间值根据测试运行的时间而变化。不好。
以下是我尝试解决此问题的摘要:
lib_datetime.now()
所指向的内容。我不知道如何进行这项工作。now()
可能取决于一个对象。或者它可以成为一个物体。我们可以创建两个对象,一个返回当前日期时间,另一个返回用于测试的固定值。我不知道如何将其注入 FastAPI app
,或者即使这是可能的。这将是我最喜欢的方法,因为这是我最熟悉的方法。now()
的运行时行为。同样,我不确定如何将其集成到测试代码中,并且我不确定这确实是一个好方法。这里的障碍是我对 FastAPI 不太熟悉,因为我最近才开始使用它。所以我还不知道我能用它做什么。
这是我学到的一些东西的总结,以及一些示例代码,将来可能对其他人有用。
Depends
Depends
概念相关的依赖注入框架。不要将其用于全局概念,例如调用 datetime.now()
。在这种情况下它不起作用Depends
概念提供了一种从 FastAPI 路径(端点)定义中解决依赖关系的方法Depends
调用之类的全局事物添加 datetime.now
类型的依赖项,则这在其他代码区域中不起作用now()
函数这样的全局事物不是一个好的设计lib_datetime/
__init__.py
fake_datetime.py
real_datetime.py
# lib_datetime/__init__.py
from lib_datetime.real_datetime import now as now_real
from lib_datetime.fake_datetime import now as now_fake
datetime_implementation_is_fake = False
def set_use_fake_now_implementation(use_fake_implementation: bool) -> None:
global datetime_implementation_is_fake
datetime_implementation_is_fake = use_fake_implementation
def now() -> datetime:
if datetime_implementation_is_fake:
return now_fake()
else:
return now_real()
# lib_datetime/fake_datetime.py
from datetime import datetime
from datetime import timezone
current_datetime = None
def now() -> datetime:
if current_datetime is None:
raise RuntimeError(f'datetime not set')
return current_datetime
def set_current_datetime(current_datetime_value: datetime) -> None:
global current_datetime
current_datetime = current_datetime_value
# lib_datetime/real_datetime.py
from datetime import datetime
from datetime import timezone
def now() -> datetime:
return datetime.now(timezone.utc)
# normal code:
from lib_datetime import now
current_dateime = now()
print(current_datetime)
# tests
from lib_datetime import set_use_fake_now_implementation
from lib_datetime.fake_datetime import set_current_datetime
current_datetime = datetime(
year=2024, month=1, day=1,
hour=9, minute=30, second=0,
tzinfo=timezone.utc,
)
set_current_datetime(current_datetime_value=current_datetime)
set_use_fake_now_implementation(True)