Skip to main content

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

fastapi_overrider-0.7.2.tar.gz (5.7 kB view details)

Uploaded Source

Built Distribution

fastapi_overrider-0.7.2-py3-none-any.whl (5.9 kB view details)

Uploaded Python 3

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

Hashes for fastapi_overrider-0.7.2.tar.gz
Algorithm Hash digest
SHA256 4f9dede4ef76afc663f578b35f4536e3433e23a7779d0335445e1916f0dbf662
MD5 5a792d88d5b03a30d26acb399c616a79
BLAKE2b-256 5f85b9e8553c31abdff67f4d654a0056a082841be428b3249c4f7eec69f72507

See more details on using hashes here.

File details

Details for the file fastapi_overrider-0.7.2-py3-none-any.whl.

File metadata

File hashes

Hashes for fastapi_overrider-0.7.2-py3-none-any.whl
Algorithm Hash digest
SHA256 1702dfb28d21f71656f03d29c2a18132d65787d0694a7691f36e9b5ffd95de45
MD5 c778bc2f10459da6650cd391b8aa6a23
BLAKE2b-256 b302697151ab6b76bb2a339e1008ab36410bd9df22c677f803cee2658805c783

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page