DEV Community

UponTheSky
UponTheSky

Posted on • Updated on

[Python] A simple guide: how to mock dependencies for unit testing in FastAPI?

TL; DR

  • Use dependency_overrides dictionary to override the dependencies set up. The fields are the arguments of Depends, and the corresponding values are callables that creates the same type of the dependency objects(the first argument of Annotated).
  • 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 RequestValidationErrors.

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!"

Enter fullscreen mode Exit fullscreen mode

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()]

Enter fullscreen mode Exit fullscreen mode

(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)]
Enter fullscreen mode Exit fullscreen mode

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
})

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Now Let’s run pytest to see the following result!

Image description

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 (2)

Collapse
 
igorshubenko profile image
Igor Shubenko

Caveat: Don't have any parameters in your mocking callables

This helped me a lot! Thanks.

Collapse
 
uponthesky profile image
UponTheSky

Glad it helped! Thank you for your kind reply :)