TL; DR
- Use
dependency_overrides
dictionary to override the dependencies set up. The fields are the arguments ofDepends
, and the corresponding values are callables that creates the same type of the dependency objects(the first argument ofAnnotated
). - Use
unittest.mock.AsyncMock
when mocking class dependencies, for your convenience. - either mocking function or class dependencies, there should be no parameters in the mocking callables. Otherwise you will get
RequestValidationError
s.
Intro
When it comes to unit testing when using the FastAPI framework, you may have to mock the dependencies you have set up. Beyond the tutorial pages, you will find the page that explains how to override dependencies.
However, the official documentation doesn't include the case of class dependencies, and there is a small pitfall you need to avoid. In this post, I want to share my experience of how I approached these issues.
1. Initial setup
For clarity, I would like to set up the most simple example that I can think of. Say we have a single endpoint “/
” for a GET request:
# 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!"
And in this router, we will use two dependencies of the following code:
# 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()]
(note that we use Annotated
here, which has been adopted by FastAPI since its version 0.95.0)
2. Mocking dependencies
Let's dive into mocking the dependencies we prepared above. We will begin with the function dependencies, an example of which is in the official documentation.
Function dependencies
Now according to the official documentation, we will write a unit testing for the router with mocked dependencies.
Remember, when you specify the list of overriding dependencies, the keys are actual functions or classes inside Depends
function(not a simple string value!), and the values are callables that generate the objects mimicking the dependency objects.
So when you mock a function dependency like the following,
def example_function(query: str = Query()) -> int:
# some code that returns `int`
ExampleDependency = Annotated[int, Depends(example_function)]
then the dependency_overrides
dictionary should be like this:
def mock_example_function(query: str = Query()):
return 42
app.dependency_overrides.update({
example_function: mock_example_function
})
Hence, our unittest code should be like the following:
# 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
Class dependencies
But what about class dependencies? Here is a pitfall: since we use a class instance as a class dependency object, we need to provide a callable that generates either an instance of that class or a mocking object of it.
So either of the following cases would be fine in our case. However, if our class has many methods to be called inside the router, then it’s better to use mockers such as unittest.mock.AsyncMock
for simplicity(well, actually that's what mock is for). Note that we don’t provide an instance itself.
# 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
})
If we choose the first option, then our client
fixture will be like:
@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)
Now Let’s run pytest
to see the following result!
Caveat: Don't have any parameters in your mocking callables
This is because, if you accidentally use arguments in your mocking callable, they're recognized as query parameters in FastAPI
This issue happened to me when I tried to simplify the mocking part with lambda expressions as follows:
@pytest.fixture
def client() -> TestClient:
app.dependency_overrides.update(
{example_function: lambda x: 42, ExampleClass: lambda x: mock.AsyncMock()}
)
return TestClient(app=app)
Then when you run pytest
, you will get this following error:
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
Since we get 422
status, we can suspect that the error is possibly from RequestValidationError
. Since it's beyond the scope of this post, we won't dig into how to check the error, but the reason is because our argument x
that we accidentally put in to the lambdas are recognized as query parameters(to see the details, check the source code).
Now that we know the exact cause of our issue, we can simplify the code with lambda expressions:
@pytest.fixture
def client() -> TestClient:
app.dependency_overrides.update(
{example_function: lambda : 42, ExampleClass: lambda : mock.AsyncMock()}
)
return TestClient(app=app)
And if you run again pytest
, the test should pass.
Conclusion
Mocking dependencies in FastAPI is not that simple as it seems. By reducing developer's workloads, FastAPI encapsulates many of the logics behind in return, and it is pretty easy to get lost once you want to implement your own logic. Hope this post helps you with testing FastAPI applications.
Top comments (4)
Caveat: part helped me a lot. Thanks for sharing
Thank you for the comment! Really glad it helped!
Caveat: Don't have any parameters in your mocking callables
This helped me a lot! Thanks.
Glad it helped! Thank you for your kind reply :)