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:
<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_successE assert FalseE + where False = <Response [422 Unprocessable Entity]>.is_successdef test_dependencies(client: TestClient): response = client.get(url="/") > assert response.is_success E assert False E + where False = <Response [422 Unprocessable Entity]>.is_successdef 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?
暂无评论内容