简化FastAPI依赖项覆盖。
项目描述
fastapi-overrider
为您的 FastAPI 测试提供简单、安全的依赖项覆盖。
安装
pip install fastapi-overrider
动机
FastAPI提供了一种很好的覆盖依赖项的机制,但也有一些需要注意的地方
- 覆盖不会自动清理,且无法轻松进行范围限定。
- 当您只想使用一些测试数据时,需要编写大量样板代码。
- 由于FastAPI在调用依赖项时依赖于对签名的检查,因此使用
unittest.mock.Mock
非常复杂。 - 同样,模拟异步依赖项也很繁琐。
fastapi-overrider的目标是使依赖项覆盖变得简单、安全、可重用、可组合和可扩展。
用法
通用用法
将其用作pytest fixture以确保每个测试都使用一组干净的覆盖运行。
override = create_fixture(app)
def test_get_item_from_value(client: TestClient, override: Overrider) -> None:
override_item = Item(item_id=0, name="Bar")
override.value(lookup_item, override_item)
response = client.get("/item/0").json()
assert Item(**response) == override_item
或者用作上下文管理器
def test_get_item_context_manager(client: TestClient, app: FastAPI) -> None:
with Overrider(app) as override:
override_item = Item(item_id=0, name="Bar")
override.value(lookup_item, override_item)
response = client.get("/item/0").json()
assert Item(**response) == override_item
在两种情况下,覆盖将在测试后清理。
上述示例还展示了如何仅用所需的返回值覆盖依赖项。Overrider将负责创建匹配的包装函数并将其设置为覆盖。
无论您的依赖项是否是异步的,Overrider都会正确处理。
基本覆盖
override.value()
返回覆盖值
def test_get_item_return_value(client: TestClient, override: Overrider) -> None:
item = override.value(lookup_item, Item(item_id=0, name="Bar"))
response = client.get("/item/0").json()
assert Item(**response) == item
override.function()
接受一个可调用的对象
def test_get_item_function(client: TestClient, override: Overrider) -> None:
item = Item(item_id=0, name="Bar")
override.function(lookup_item, lambda item_id: item) # noqa: ARG005
response = client.get("/item/0").json()
assert Item(**response) == item
将其用作app.dependency_overrides
的替代品
def test_get_item_drop_in(client: TestClient, override: Overrider) -> None:
item = Item(item_id=0, name="Bar")
def override_lookup_item(item_id: int) -> Item: # noqa: ARG001
return item
override[lookup_item] = override_lookup_item
response = client.get("/item/0").json()
assert Item(**response) == item
模拟和间谍
Overrider可以为您创建模拟
def test_get_item_mock(client: TestClient, override: Overrider) -> None:
item = Item(item_id=0, name="Bar")
mock_lookup = override.mock(lookup_item)
mock_lookup.return_value = item
response = client.get("/item/0")
mock_lookup.assert_called_once_with(item_id=0)
assert Item(**response.json()) == item
监视依赖项。原始依赖项仍然会被调用,但您可以调用断言并像unittest.mock.Mock
一样检查它
def test_get_item_spy(client: TestClient, override: Overrider) -> None:
spy = override.spy(lookup_item)
client.get("/item/0")
spy.assert_called_with(item_id=0)
自动生成的覆盖
Overrider可以使用Unifactory自动生成模拟对象。
要启用此额外功能,请使用pip install fastapi-overrider[unifactory]
。
Overrider将自动使用来自Polyfactory的库存的匹配工厂来处理给定依赖项。
生成单个覆盖值。您可以向任何自动生成方法提供可选关键字参数,以将属性固定到特定值,如本例中的name
def test_get_some_item(client: TestClient, override: Overrider) -> None:
item = override.some(lookup_item, name="Foo")
response = client.get(f"/item/{item.item_id}")
assert item.name == "Foo"
assert item == Item(**response.json())
您也可以让Overrider生成多个覆盖值
def test_get_five_items(client: TestClient, override: Overrider) -> None:
items = override.batch(lookup_item, 5)
for item in items:
response = client.get(f"/item/{item.item_id}")
assert item == Item(**response.json())
尝试涵盖模型可以采取的全部形式
def test_cover_get_items(client: TestClient, override: Overrider) -> None:
items = override.cover(lookup_item)
for item in items:
response = client.get(f"/item/{item.item_id}")
assert item == Item(**response.json())
快捷方式
您可以直接调用Overrider,它会猜测您要做什么
如果您传递一个可调用的对象,它将像override.function()
一样行动
def test_get_item_call_callable(client: TestClient, override: Overrider) -> None:
item = Item(item_id=0, name="Bar")
override(lookup_item, lambda item_id: item) # noqa: ARG005
response = client.get("/item/0").json()
assert Item(**response) == item
如果您传递一个不可调用的对象,它将像override.value()
一样行动
def test_get_item_call_value(client: TestClient, override: Overrider) -> None:
item = override(lookup_item, Item(item_id=0, name="Bar"))
response = client.get("/item/0").json()
assert Item(**response) == item
如果您不传递任何内容,它将创建一个模拟
def test_get_item_call_mock(client: TestClient, override: Overrider) -> None:
item = Item(item_id=0, name="Bar")
mock_lookup = override(lookup_item)
mock_lookup.return_value = item
response = client.get("/item/0")
mock_lookup.assert_called_once_with(item_id=0)
assert Item(**response.json()) == item
高级模式
重用常见的覆盖。它们是可组合的,您可以拥有多个
@pytest.fixture()
def as_dave(app: FastAPI) -> Iterator[Overrider]:
with Overrider(app) as override:
override(get_user, User(name="Dave", authenticated=True))
yield override
@pytest.fixture()
def in_the_morning(app: FastAPI) -> Iterator[Overrider]:
with Overrider(app) as override:
override(get_time_of_day, "morning")
yield override
def test_get_greeting(client: TestClient, as_dave: Overrider, in_the_morning: Overrider) -> None:
response = client.get("/")
assert response.text == '"Good morning, Dave."'
使用您自己的便利方法扩展它
class MyOverrider(Overrider):
def user(self, *, name: str, authenticated: bool = False) -> None:
self(get_user, User(name=name, authenticated=authenticated))
@pytest.fixture()
def override(app: FastAPI):
with MyOverrider(app) as override:
yield override
def test_open_pod_bay_doors(client: TestClient, my_override: MyOverrider) -> None:
my_override.user(name="Dave", authenticated=False)
response = client.get("/open/pod_bay_doors")
assert response.text == "\"I'm afraid I can't let you do that, Dave.\""