[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:

<span># example.py </span><span>from</span> <span>fastapi</span> <span>import</span> <span>FastAPI</span>
<span>from</span> <span>.dependencies</span> <span>import</span> <span>ExampleFunctionDependency</span><span>,</span> <span>ExampleClassDependency</span>
<span>app</span> <span>=</span> <span>FastAPI</span><span>()</span>
<span>@app.get</span><span>(</span><span>"</span><span>/</span><span>"</span><span>)</span>
<span>def</span> <span>example_router</span><span>(</span>
<span>*</span><span>,</span>
<span>example_function_dependency</span><span>:</span> <span>ExampleFunctionDependency</span><span>,</span>
<span>example_class_dependency</span><span>:</span> <span>ExampleClassDependency</span>
<span>):</span>
<span>return</span> <span>"</span><span>example dependency!</span><span>"</span>
<span># example.py </span><span>from</span> <span>fastapi</span> <span>import</span> <span>FastAPI</span>

<span>from</span> <span>.dependencies</span> <span>import</span> <span>ExampleFunctionDependency</span><span>,</span> <span>ExampleClassDependency</span>

<span>app</span> <span>=</span> <span>FastAPI</span><span>()</span>


<span>@app.get</span><span>(</span><span>"</span><span>/</span><span>"</span><span>)</span>
<span>def</span> <span>example_router</span><span>(</span>
    <span>*</span><span>,</span>
    <span>example_function_dependency</span><span>:</span> <span>ExampleFunctionDependency</span><span>,</span>
    <span>example_class_dependency</span><span>:</span> <span>ExampleClassDependency</span>
<span>):</span>
    <span>return</span> <span>"</span><span>example dependency!</span><span>"</span>
# 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:

<span># dependencies.py </span><span>from</span> <span>typing</span> <span>import</span> <span>Annotated</span>
<span>from</span> <span>fastapi</span> <span>import</span> <span>Depends</span>
<span>def</span> <span>example_function</span><span>()</span> <span>-></span> <span>int</span><span>:</span>
<span>return</span> <span>1</span>
<span>class</span> <span>ExampleClass</span><span>:</span>
<span>...</span>
<span>ExampleFunctionDependency</span> <span>=</span> <span>Annotated</span><span>[</span><span>int</span><span>,</span> <span>Depends</span><span>(</span><span>example_function</span><span>)]</span>
<span>ExampleClassDependency</span> <span>=</span> <span>Annotated</span><span>[</span><span>ExampleClass</span><span>,</span> <span>Depends</span><span>()]</span>
<span># dependencies.py </span><span>from</span> <span>typing</span> <span>import</span> <span>Annotated</span>

<span>from</span> <span>fastapi</span> <span>import</span> <span>Depends</span>


<span>def</span> <span>example_function</span><span>()</span> <span>-></span> <span>int</span><span>:</span>
    <span>return</span> <span>1</span>


<span>class</span> <span>ExampleClass</span><span>:</span>
    <span>...</span>


<span>ExampleFunctionDependency</span> <span>=</span> <span>Annotated</span><span>[</span><span>int</span><span>,</span> <span>Depends</span><span>(</span><span>example_function</span><span>)]</span>
<span>ExampleClassDependency</span> <span>=</span> <span>Annotated</span><span>[</span><span>ExampleClass</span><span>,</span> <span>Depends</span><span>()]</span>
# 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,

<span>def</span> <span>example_function</span><span>(</span><span>query</span><span>:</span> <span>str</span> <span>=</span> <span>Query</span><span>())</span> <span>-></span> <span>int</span><span>:</span>
<span># some code that returns `int` </span>
<span>ExampleDependency</span> <span>=</span> <span>Annotated</span><span>[</span><span>int</span><span>,</span> <span>Depends</span><span>(</span><span>example_function</span><span>)]</span>
<span>def</span> <span>example_function</span><span>(</span><span>query</span><span>:</span> <span>str</span> <span>=</span> <span>Query</span><span>())</span> <span>-></span> <span>int</span><span>:</span>
  <span># some code that returns `int` </span>
<span>ExampleDependency</span> <span>=</span> <span>Annotated</span><span>[</span><span>int</span><span>,</span> <span>Depends</span><span>(</span><span>example_function</span><span>)]</span>
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:

<span>def</span> <span>mock_example_function</span><span>(</span><span>query</span><span>:</span> <span>str</span> <span>=</span> <span>Query</span><span>()):</span>
<span>return</span> <span>42</span>
<span>app</span><span>.</span><span>dependency_overrides</span><span>.</span><span>update</span><span>({</span>
<span>example_function</span><span>:</span> <span>mock_example_function</span>
<span>})</span>
<span>def</span> <span>mock_example_function</span><span>(</span><span>query</span><span>:</span> <span>str</span> <span>=</span> <span>Query</span><span>()):</span>
  <span>return</span> <span>42</span>

<span>app</span><span>.</span><span>dependency_overrides</span><span>.</span><span>update</span><span>({</span>
  <span>example_function</span><span>:</span> <span>mock_example_function</span>
<span>})</span>
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:

<span># test_example.py </span><span>from</span> <span>unittest</span> <span>import</span> <span>mock</span>
<span>import</span> <span>pytest</span>
<span>from</span> <span>fastapi.testclient</span> <span>import</span> <span>TestClient</span>
<span>from</span> <span>.example</span> <span>import</span> <span>app</span>
<span>from</span> <span>.dependencies</span> <span>import</span> <span>example_function</span><span>,</span> <span>ExampleClass</span>
<span>@pytest.fixture</span>
<span>def</span> <span>client</span><span>()</span> <span>-></span> <span>TestClient</span><span>:</span>
<span>def</span> <span>mock_example_function</span><span>()</span> <span>-></span> <span>int</span><span>:</span>
<span>return</span> <span>42</span>
<span>app</span><span>.</span><span>dependency_overrides</span><span>.</span><span>update</span><span>(</span>
<span>{</span><span>example_function</span><span>:</span> <span>mock_example_function</span><span>}</span>
<span>)</span>
<span>return</span> <span>TestClient</span><span>(</span><span>app</span><span>=</span><span>app</span><span>)</span>
<span>def</span> <span>test_dependencies</span><span>(</span><span>client</span><span>:</span> <span>TestClient</span><span>):</span>
<span>response</span> <span>=</span> <span>client</span><span>.</span><span>get</span><span>(</span><span>"</span><span>/</span><span>"</span><span>)</span>
<span>assert</span> <span>response</span><span>.</span><span>is_success</span>
<span># test_example.py </span><span>from</span> <span>unittest</span> <span>import</span> <span>mock</span>

<span>import</span> <span>pytest</span>
<span>from</span> <span>fastapi.testclient</span> <span>import</span> <span>TestClient</span>

<span>from</span> <span>.example</span> <span>import</span> <span>app</span>
<span>from</span> <span>.dependencies</span> <span>import</span> <span>example_function</span><span>,</span> <span>ExampleClass</span>


<span>@pytest.fixture</span>
<span>def</span> <span>client</span><span>()</span> <span>-></span> <span>TestClient</span><span>:</span>
    <span>def</span> <span>mock_example_function</span><span>()</span> <span>-></span> <span>int</span><span>:</span>
        <span>return</span> <span>42</span>

    <span>app</span><span>.</span><span>dependency_overrides</span><span>.</span><span>update</span><span>(</span>
        <span>{</span><span>example_function</span><span>:</span> <span>mock_example_function</span><span>}</span>
    <span>)</span>

    <span>return</span> <span>TestClient</span><span>(</span><span>app</span><span>=</span><span>app</span><span>)</span>


<span>def</span> <span>test_dependencies</span><span>(</span><span>client</span><span>:</span> <span>TestClient</span><span>):</span>
    <span>response</span> <span>=</span> <span>client</span><span>.</span><span>get</span><span>(</span><span>"</span><span>/</span><span>"</span><span>)</span>
    <span>assert</span> <span>response</span><span>.</span><span>is_success</span>
# 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.

<span># a function that provides either mocking object or an instance of an object </span><span>def</span> <span>mock_example_class</span><span>()</span> <span>-></span> <span>ExampleClass</span><span>:</span>
<span>return</span> <span>mock</span><span>.</span><span>AsyncMock</span><span>()</span>
<span>app</span><span>.</span><span>dependency_overrides</span><span>.</span><span>update</span><span>({</span>
<span>ExampleClass</span><span>:</span> <span>mock_example_class</span>
<span>})</span>
<span># directly passes our custom mocking class </span><span>class</span> <span>CustomMockingClass</span><span>:</span>
<span></span>
<span>app</span><span>.</span><span>dependency_overrides</span><span>.</span><span>update</span><span>({</span>
<span>ExampleClass</span><span>:</span> <span>CustomMockingClass</span>
<span>})</span>
<span># a function that provides either mocking object or an instance of an object </span><span>def</span> <span>mock_example_class</span><span>()</span> <span>-></span> <span>ExampleClass</span><span>:</span>
        <span>return</span> <span>mock</span><span>.</span><span>AsyncMock</span><span>()</span>

<span>app</span><span>.</span><span>dependency_overrides</span><span>.</span><span>update</span><span>({</span> 
  <span>ExampleClass</span><span>:</span> <span>mock_example_class</span>
<span>})</span>

<span># directly passes our custom mocking class </span><span>class</span> <span>CustomMockingClass</span><span>:</span>
  <span>…</span>

<span>app</span><span>.</span><span>dependency_overrides</span><span>.</span><span>update</span><span>({</span> 
  <span>ExampleClass</span><span>:</span> <span>CustomMockingClass</span>
<span>})</span>
# 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:

<span>@pytest.fixture</span>
<span>def</span> <span>client</span><span>()</span> <span>-></span> <span>TestClient</span><span>:</span>
<span>def</span> <span>mock_example_function</span><span>()</span> <span>-></span> <span>int</span><span>:</span>
<span>return</span> <span>42</span>
<span>def</span> <span>mock_example_class</span><span>()</span> <span>-></span> <span>mock</span><span>.</span><span>AsyncMock</span><span>:</span>
<span>return</span> <span>mock</span><span>.</span><span>AsyncMock</span><span>()</span>
<span>app</span><span>.</span><span>dependency_overrides</span><span>.</span><span>update</span><span>(</span>
<span>{</span><span>example_function</span><span>:</span> <span>mock_example_function</span><span>,</span> <span>ExampleClass</span><span>:</span> <span>mock_example_class</span><span>}</span>
<span>)</span>
<span>return</span> <span>TestClient</span><span>(</span><span>app</span><span>=</span><span>app</span><span>)</span>
<span>@pytest.fixture</span>
<span>def</span> <span>client</span><span>()</span> <span>-></span> <span>TestClient</span><span>:</span>
    <span>def</span> <span>mock_example_function</span><span>()</span> <span>-></span> <span>int</span><span>:</span>
        <span>return</span> <span>42</span>

    <span>def</span> <span>mock_example_class</span><span>()</span> <span>-></span> <span>mock</span><span>.</span><span>AsyncMock</span><span>:</span>
        <span>return</span> <span>mock</span><span>.</span><span>AsyncMock</span><span>()</span>

    <span>app</span><span>.</span><span>dependency_overrides</span><span>.</span><span>update</span><span>(</span>
        <span>{</span><span>example_function</span><span>:</span> <span>mock_example_function</span><span>,</span> <span>ExampleClass</span><span>:</span> <span>mock_example_class</span><span>}</span>
    <span>)</span>

    <span>return</span> <span>TestClient</span><span>(</span><span>app</span><span>=</span><span>app</span><span>)</span>
@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!

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:

<span>@pytest.fixture</span>
<span>def</span> <span>client</span><span>()</span> <span>-></span> <span>TestClient</span><span>:</span>
<span>app</span><span>.</span><span>dependency_overrides</span><span>.</span><span>update</span><span>(</span>
<span>{</span><span>example_function</span><span>:</span> <span>lambda</span> <span>x</span><span>:</span> <span>42</span><span>,</span> <span>ExampleClass</span><span>:</span> <span>lambda</span> <span>x</span><span>:</span> <span>mock</span><span>.</span><span>AsyncMock</span><span>()}</span>
<span>)</span>
<span>return</span> <span>TestClient</span><span>(</span><span>app</span><span>=</span><span>app</span><span>)</span>
<span>@pytest.fixture</span>
<span>def</span> <span>client</span><span>()</span> <span>-></span> <span>TestClient</span><span>:</span>
    <span>app</span><span>.</span><span>dependency_overrides</span><span>.</span><span>update</span><span>(</span>
        <span>{</span><span>example_function</span><span>:</span> <span>lambda</span> <span>x</span><span>:</span> <span>42</span><span>,</span> <span>ExampleClass</span><span>:</span> <span>lambda</span> <span>x</span><span>:</span> <span>mock</span><span>.</span><span>AsyncMock</span><span>()}</span>
    <span>)</span>

    <span>return</span> <span>TestClient</span><span>(</span><span>app</span><span>=</span><span>app</span><span>)</span>
@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
    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
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:

<span>@pytest.fixture</span>
<span>def</span> <span>client</span><span>()</span> <span>-></span> <span>TestClient</span><span>:</span>
<span>app</span><span>.</span><span>dependency_overrides</span><span>.</span><span>update</span><span>(</span>
<span>{</span><span>example_function</span><span>:</span> <span>lambda</span> <span>:</span> <span>42</span><span>,</span> <span>ExampleClass</span><span>:</span> <span>lambda</span> <span>:</span> <span>mock</span><span>.</span><span>AsyncMock</span><span>()}</span>
<span>)</span>
<span>return</span> <span>TestClient</span><span>(</span><span>app</span><span>=</span><span>app</span><span>)</span>
<span>@pytest.fixture</span>
<span>def</span> <span>client</span><span>()</span> <span>-></span> <span>TestClient</span><span>:</span>
    <span>app</span><span>.</span><span>dependency_overrides</span><span>.</span><span>update</span><span>(</span>
        <span>{</span><span>example_function</span><span>:</span> <span>lambda</span> <span>:</span> <span>42</span><span>,</span> <span>ExampleClass</span><span>:</span> <span>lambda</span> <span>:</span> <span>mock</span><span>.</span><span>AsyncMock</span><span>()}</span>
    <span>)</span>

    <span>return</span> <span>TestClient</span><span>(</span><span>app</span><span>=</span><span>app</span><span>)</span>
@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.

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

© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
Dream most deep place, only then the smile is not tired.
梦的最深处,只有微笑不累
评论 抢沙发

请登录后发表评论

    暂无评论内容