覆盖 FastAPI 依赖项以进行测试的最佳方法,每个测试使用不同的依赖项

根据 FastAPI official documentation ,覆盖测试依赖项的推荐方法是在所有测试运行之前全局执行:

    async def override_dependency(q: Optional[str] = None):
        return {"q": q, "skip": 5, "limit": 10}

    app.dependency_overrides[common_parameters] = override_dependency

    def test_override_in_items():
        response = client.get("/items/")
        assert response.status_code == 200
        assert response.json() == {
            "message": "Hello Items!",
            "params": {"q": None, "skip": 5, "limit": 10},
        }

    def test_override_in_items_with_q():
        response = client.get("/items/?q=foo")
        assert response.status_code == 200
        assert response.json() == {
            "message": "Hello Items!",
            "params": {"q": "foo", "skip": 5, "limit": 10},
        }

但这允许您在整个测试运行中只覆盖一次依赖项。如果我需要为每个测试提供不同的依赖项怎么办?覆盖测试主体内部的依赖关系并在测试完成后重置它们是否安全?像这样

def test_override_in_items():

    app.dependency_overrides[common_parameters] = override_dependency

    response = client.get("/items/")
    assert response.status_code == 200
    assert response.json() == {
        "message": "Hello Items!",
        "params": {"q": None, "skip": 5, "limit": 10},
    }

    app.dependency_overrides[common_parameters] = {}

像上面的例子那样做有什么缺点?

stack overflow Best way to override FastAPI dependencies for testing with a different dependency for each test
原文答案

答案:

作者头像

这就是我在测试中使用自己来覆盖依赖项的方法。您必须将其用作带有 with 语句的上下文管理器(参见下面的示例)。进入上下文后,它会保存遇到的现有覆盖。这些将在退出上下文时恢复。

import typing
from fastapi import FastAPI

class DependencyOverrider:
    def __init__(
        self, app: FastAPI, overrides: typing.Mapping[typing.Callable, typing.Callable]
    ) -> None:
        self.overrides = overrides
        self._app = app
        self._old_overrides = {}

    def __enter__(self):
        for dep, new_dep in self.overrides.items():
            if dep in self._app.dependency_overrides:
                # Save existing overrides
                self._old_overrides[dep] = self._app.dependency_overrides[dep]
            self._app.dependency_overrides[dep] = new_dep
        return self

    def __exit__(self, *args: typing.Any) -> None:
        for dep in self.overrides.keys():
            if dep in self._old_overrides:
                # Restore previous overrides
                self._app.dependency_overrides[dep] = self._old_overrides.pop(dep)
            else:
                # Just delete the entry
                del self._app.dependency_overrides[dep]

您可以在测试中像这样使用它:

from app.main import app
from fastapi.testclient import TestClient

def test_override_in_items(client: TestClient):
    with DependencyOverrider(app, overrides={common_parameters: override_dependency}):
        response = client.get("/items/")
        assert response.status_code == 200
        assert response.json() == {
            "message": "Hello Items!",
            "params": {"q": None, "skip": 5, "limit": 10},
        }

或者可以将其用作 pytest 中的固定装置:


@pytest.fixture(scope="function")
def faked_common_parameters():
    with DependencyOverrider(
        app, overrides={common_parameters: override_dependency}
    ) as overrider:
        yield overrider

def test_override_in_items(client: TestClient, faked_common_parameters):
    # Running with the overridden dependency 
    response = client.get("/items/")
    assert response.status_code == 200
    assert response.json() == {
        "message": "Hello Items!",
        "params": {"q": None, "skip": 5, "limit": 10},
    }
作者头像

您可以通过将 app.dependency_overrides 设置为空 dict: 来重置您的覆盖

app.dependency_overrides = {}
作者头像

根据官方文档: https://fastapi.tiangolo.com/advanced/testing-dependencies/

如果您只想在某些测试期间覆盖依赖项,则可以在测试开始时(在测试功能内部)设置覆盖率,并在末尾(测试功能的末尾)重置它。

基于此,我们可以得出结论,该框架的作者没有看到任何危险。

但是用户 Vlad 是正确的。您可能具有多个测试的共同点,并且仅在一个测试中使用的依赖项。在一个测试中删除了所有依赖项后,您将不会传递使用常见依赖性的后续依赖项。这可以归因于不便。

因此,我认为为方便起见,最好使用提出的某种辅助机制 here

作者头像

我使用了 Mat's answer 并创建了一个开源库,它根据代码片段添加了一个固定装置。看到它 here

要使用该库,只需执行: pip install pytest-fastapi-deps ,然后您将拥有 fastapi_dep 固定装置。像这样使用它:

import pytest
from fastapi.testclient import TestClient

client = TestClient(app)

def test_get_override_single_dep(fastapi_dep):
    with fastapi_dep(app).override({common_parameters: lambda: {"my": "override"}}):
        response = client.get("/items/")
        assert response.status_code == 200
        assert response.json() == {"my": "override"}