FastAPI Dependency overrides made easy.
Project description
fastapi-overrider
Easy and safe dependency overrides for your FastAPI tests.
Installation
pip install fastapi-overrider
Motivation
FastAPI provided a nice mechanism to override dependencies, but there are a few gotchas:
- Overrides are not cleaned up automatically and can't be scoped easily.
- Lots of boilerplate code required when you just want some test data.
- Using
unittest.mock.Mock
is non-trivial due to the way FastAPI relies on inspection of signatures when calling dependencies. - Likewise, mocking async dependencies is cumbersome.
The goal of fastapi-override is to make dependency overriding easy, safe, reusable, composable, and extendable.
Usage
General usage
Use it as pytest fixture to ensure every test is run with a clean set of overrides.
override = create_fixture(app)
def test_get_item_from_value(client: TestClient, override: Overrider) -> None:
override_item = Item(item_id=0, name="Bar")
override.value(lookup_item, override_item)
response = client.get("/item/0").json()
assert Item(**response) == override_item
Alternatively use it as a context manager:
def test_get_item_context_manager(client: TestClient, app: FastAPI) -> None:
with Overrider(app) as override:
override_item = Item(item_id=0, name="Bar")
override.value(lookup_item, override_item)
response = client.get("/item/0").json()
assert Item(**response) == override_item
In both cases the overrides will be cleaned up after the test.
The above examples also show how to override a dependency with just the desired return value. Overrider will take care of creating a matching wrapper function and setting it as an override.
It doesn't matter if your dependency is async or not. Overrider will do the right thing.
Basic overrides
override.value()
returns the override value:
def test_get_item_return_value(client: TestClient, override: Overrider) -> None:
item = override.value(lookup_item, Item(item_id=0, name="Bar"))
response = client.get("/item/0").json()
assert Item(**response) == item
override.function()
accepts a callable:
def test_get_item_function(client: TestClient, override: Overrider) -> None:
item = Item(item_id=0, name="Bar")
override.function(lookup_item, lambda item_id: item) # noqa: ARG005
response = client.get("/item/0").json()
assert Item(**response) == item
Use it as a drop-in replacement for app.dependency_overrides
:
def test_get_item_drop_in(client: TestClient, override: Overrider) -> None:
item = Item(item_id=0, name="Bar")
def override_lookup_item(item_id: int) -> Item: # noqa: ARG001
return item
override[lookup_item] = override_lookup_item
response = client.get("/item/0").json()
assert Item(**response) == item
Mocks and spies
Overrider can create mocks for you:
def test_get_item_mock(client: TestClient, override: Overrider) -> None:
item = Item(item_id=0, name="Bar")
mock_lookup = override.mock(lookup_item)
mock_lookup.return_value = item
response = client.get("/item/0")
mock_lookup.assert_called_once_with(item_id=0)
assert Item(**response.json()) == item
Spy on a dependency. The original dependency will still be called, but you can call assertions
and inspect it like a unittest.mock.Mock
:
def test_get_item_spy(client: TestClient, override: Overrider) -> None:
spy = override.spy(lookup_item)
client.get("/item/0")
spy.assert_called_with(item_id=0)
Auto-generated overrides
Overrider can auto-generate mock objects using Unifactory.
To enable this extra feature, use pip install fastapi-overrider[unifactory]
.
Overrider will automatically use a matching factory from Polyfactory's inventory for the given dependency.
Generate a single override value. You can provide optional keyword arguments to any of the
auto-generator methods in order to pin an attribute to a specific value, like name
in
this example:
def test_get_some_item(client: TestClient, override: Overrider) -> None:
item = override.some(lookup_item, name="Foo")
response = client.get(f"/item/{item.item_id}")
assert item.name == "Foo"
assert item == Item(**response.json())
You can also let Overrider generate multiple override values:
def test_get_five_items(client: TestClient, override: Overrider) -> None:
items = override.batch(lookup_item, 5)
for item in items:
response = client.get(f"/item/{item.item_id}")
assert item == Item(**response.json())
Attempt to cover the full range of forms that a model can take:
def test_cover_get_items(client: TestClient, override: Overrider) -> None:
items = override.cover(lookup_item)
for item in items:
response = client.get(f"/item/{item.item_id}")
assert item == Item(**response.json())
Shortcuts
You can call Overrider directly and it will guess what you want to do:
If you pass in a callable, it will act like override.function()
:
def test_get_item_call_callable(client: TestClient, override: Overrider) -> None:
item = Item(item_id=0, name="Bar")
override(lookup_item, lambda item_id: item) # noqa: ARG005
response = client.get("/item/0").json()
assert Item(**response) == item
If you pass in a non-callable, it will act like override.value()
:
def test_get_item_call_value(client: TestClient, override: Overrider) -> None:
item = override(lookup_item, Item(item_id=0, name="Bar"))
response = client.get("/item/0").json()
assert Item(**response) == item
If you don't pass in anything, it will create a mock:
def test_get_item_call_mock(client: TestClient, override: Overrider) -> None:
item = Item(item_id=0, name="Bar")
mock_lookup = override(lookup_item)
mock_lookup.return_value = item
response = client.get("/item/0")
mock_lookup.assert_called_once_with(item_id=0)
assert Item(**response.json()) == item
Advanced patterns
Reuse common overrides. They are composable, you can have multiple:
@pytest.fixture()
def as_dave(app: FastAPI) -> Iterator[Overrider]:
with Overrider(app) as override:
override(get_user, User(name="Dave", authenticated=True))
yield override
@pytest.fixture()
def in_the_morning(app: FastAPI) -> Iterator[Overrider]:
with Overrider(app) as override:
override(get_time_of_day, "morning")
yield override
def test_get_greeting(client: TestClient, as_dave: Overrider, in_the_morning: Overrider) -> None:
response = client.get("/")
assert response.text == '"Good morning, Dave."'
Extend it with your own convenience methods:
class MyOverrider(Overrider):
def user(self, *, name: str, authenticated: bool = False) -> None:
self(get_user, User(name=name, authenticated=authenticated))
@pytest.fixture()
def override(app: FastAPI):
with MyOverrider(app) as override:
yield override
def test_open_pod_bay_doors(client: TestClient, my_override: MyOverrider) -> None:
my_override.user(name="Dave", authenticated=False)
response = client.get("/open/pod_bay_doors")
assert response.text == "\"I'm afraid I can't let you do that, Dave.\""
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file fastapi_overrider-0.7.2.tar.gz
.
File metadata
- Download URL: fastapi_overrider-0.7.2.tar.gz
- Upload date:
- Size: 5.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.7.1 CPython/3.12.1 Darwin/23.1.0
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 4f9dede4ef76afc663f578b35f4536e3433e23a7779d0335445e1916f0dbf662 |
|
MD5 | 5a792d88d5b03a30d26acb399c616a79 |
|
BLAKE2b-256 | 5f85b9e8553c31abdff67f4d654a0056a082841be428b3249c4f7eec69f72507 |
File details
Details for the file fastapi_overrider-0.7.2-py3-none-any.whl
.
File metadata
- Download URL: fastapi_overrider-0.7.2-py3-none-any.whl
- Upload date:
- Size: 5.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.7.1 CPython/3.12.1 Darwin/23.1.0
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 1702dfb28d21f71656f03d29c2a18132d65787d0694a7691f36e9b5ffd95de45 |
|
MD5 | c778bc2f10459da6650cd391b8aa6a23 |
|
BLAKE2b-256 | b302697151ab6b76bb2a339e1008ab36410bd9df22c677f803cee2658805c783 |