tl;博士
- 使用
dependency_overrides
字典来覆盖设置的依赖关系。字段是Depends
的参数,相应的值是可呼出的值,它创建了相同类型的依赖项对象(Annotated
的第一个参数)。 - 为您的方便起见,在嘲笑课程依赖时使用
unittest.mock.AsyncMock
。 - 模仿功能或类依赖性,模型可可中应该没有参数。否则您将获得
RequestValidationError
。
介绍
使用FastAPI框架时进行单元测试时,您可能必须模拟设置的依赖项。除了tutorial pages之外,您还将找到解释如何覆盖dependencies的页面。
但是,官方文档不包括班级依赖的情况,并且您需要避免一个较小的陷阱。在这篇文章中,我想分享我如何解决这些问题的经验。
1.初始设置
为了清楚起见,我想设置我能想到的最简单的示例。假设我们有一个端点 - /
âor for get请求:
# example.py
from fastapi import FastAPI
from .dependencies import ExampleFunctionDependency, ExampleClassDependency
app = FastAPI()
@app.get("/")
def example_router(
*,
example_function_dependency: ExampleFunctionDependency,
example_class_dependency: ExampleClassDependency
):
return "example dependency!"
在此路由器中,我们将使用以下代码的两个依赖项:
# dependencies.py
from typing import Annotated
from fastapi import Depends
def example_function() -> int:
return 1
class ExampleClass:
...
ExampleFunctionDependency = Annotated[int, Depends(example_function)]
ExampleClassDependency = Annotated[ExampleClass, Depends()]
(请注意,我们在此处使用Annotated
,这已被Fastapi since its version 0.95.0 )
2.嘲笑依赖性
让我们潜入嘲笑上面准备的依赖项。我们将从函数依赖项开始,该函数在官方文档中的示例。
功能依赖性
现在根据official documentation,我们将为路由器编写一个模拟依赖的路由器。
请记住,当您指定覆盖依赖项的列表时,键是Depends
函数中的实际功能或类(不是简单的字符串值!),值为 callable 生成模仿依赖项对象的对象。
因此,当您像以下内容一样模拟函数依赖关系时,
def example_function(query: str = Query()) -> int:
# some code that returns `int`
ExampleDependency = Annotated[int, Depends(example_function)]
那么dependency_overrides
字典应该是这样的:
def mock_example_function(query: str = Query()):
return 42
app.dependency_overrides.update({
example_function: mock_example_function
})
因此,我们的UNITSEST代码应该如下:
# test_example.py
from unittest import mock
import pytest
from fastapi.testclient import TestClient
from .example import app
from .dependencies import example_function, ExampleClass
@pytest.fixture
def client() -> TestClient:
def mock_example_function() -> int:
return 42
app.dependency_overrides.update(
{example_function: mock_example_function}
)
return TestClient(app=app)
def test_dependencies(client: TestClient):
response = client.get("/")
assert response.is_success
班级依赖性
但是班级依赖性呢?这是一个陷阱:由于我们将类实例用作类依赖性对象,因此我们需要提供一个可召唤的对象,该可callable生成该类的实例或IT的模拟对象。
因此,在我们的情况下,以下任何一种情况都可以。但是,如果我们的班级在路由器内有许多方法要调用,那么最好使用嘲笑器(例如unittest.mock.AsyncMock
)来简单起见(嗯,实际上,这就是模拟的目的)。请注意,我们不提供实例本身。
# a function that provides either mocking object or an instance of an object
def mock_example_class() -> ExampleClass:
return mock.AsyncMock()
app.dependency_overrides.update({
ExampleClass: mock_example_class
})
# directly passes our custom mocking class
class CustomMockingClass:
…
app.dependency_overrides.update({
ExampleClass: CustomMockingClass
})
如果我们选择了第一个选项,那么我们的client
固定装置将是:
@pytest.fixture
def client() -> TestClient:
def mock_example_function() -> int:
return 42
def mock_example_class() -> mock.AsyncMock:
return mock.AsyncMock()
app.dependency_overrides.update(
{example_function: mock_example_function, ExampleClass: mock_example_class}
)
return TestClient(app=app)
现在让我们运行pytest
查看以下结果!
警告:您的模拟可可中没有任何参数
这是因为,如果您在模拟可叫中意外使用参数,则将其视为FastApi
中的查询参数当我试图用lambda表达式简化模拟部分时,我发生了这个问题:
@pytest.fixture
def client() -> TestClient:
app.dependency_overrides.update(
{example_function: lambda x: 42, ExampleClass: lambda x: mock.AsyncMock()}
)
return TestClient(app=app)
然后,当您运行pytest
时,您将获得以下错误:
def test_dependencies(client: TestClient):
response = client.get(url="/")
> assert response.is_success
E assert False
E + where False = <Response [422 Unprocessable Entity]>.is_success
由于我们获得了422
状态,因此我们可以怀疑该错误可能来自RequestValidationError
。由于它超出了这篇文章的范围,因此我们不会挖掘如何检查错误,但原因是因为我们的参数x
我们意外地放入lambdas被确认为查询参数(要查看详细信息,请检查详细信息,检查source code)。
现在我们知道了问题的确切原因,我们可以用lambda表达式简化代码:
@pytest.fixture
def client() -> TestClient:
app.dependency_overrides.update(
{example_function: lambda : 42, ExampleClass: lambda : mock.AsyncMock()}
)
return TestClient(app=app)
,如果您再次运行pytest
,则测试应通过。
结论
FastApi中的嘲笑依赖性并不是看起来那么简单。通过减少开发人员的工作负载,Fastapi封装了许多逻辑以回报,一旦您想实现自己的逻辑,就很容易迷路。希望这篇文章可以帮助您测试FastAPI应用程序。