# fastapi-overrider
Easy and safe dependency overrides for your [FastAPI](https://fastapi.tiangolo.com/) 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.
```python
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:
```python
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:
```python
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:
```python
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`:
```python
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:
```python
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`:
```python
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](https://github.com/phha/unifactory).
To enable this extra feature, use `pip install fastapi-overrider[unifactory]`.
Overrider will automatically use a matching factory from
[Polyfactory's inventory](https://polyfactory.litestar.dev/usage/library_factories/index.html)
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:
```python
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:
```python
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:
```python
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()`:
```python
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()`:
```python
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:
```python
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:
```python
@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:
```python
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.\""
```
Raw data
{
"_id": null,
"home_page": "https://github.com/phha/fastapi-overrider",
"name": "fastapi-overrider",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.10,<4.0",
"maintainer_email": "",
"keywords": "fastapi,pytest",
"author": "Philipp Hack",
"author_email": "philipp.hack@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/5f/85/b9e8553c31abdff67f4d654a0056a082841be428b3249c4f7eec69f72507/fastapi_overrider-0.7.2.tar.gz",
"platform": null,
"description": "# fastapi-overrider\n\nEasy and safe dependency overrides for your [FastAPI](https://fastapi.tiangolo.com/) tests. \n\n## Installation\n\n`pip install fastapi-overrider`\n\n## Motivation\n\nFastAPI provided a nice mechanism to override dependencies, but there are a few gotchas:\n\n- Overrides are not cleaned up automatically and can't be scoped easily.\n- Lots of boilerplate code required when you just want *some* test data.\n- Using `unittest.mock.Mock` is non-trivial due to the way FastAPI relies on inspection\n of signatures when calling dependencies.\n- Likewise, mocking async dependencies is cumbersome.\n\nThe goal of fastapi-override is to make dependency overriding easy, safe, reusable, composable,\nand extendable.\n\n## Usage\n\n### General usage\n\nUse it as pytest fixture to ensure every test is run with a clean set of overrides.\n\n```python\noverride = create_fixture(app)\n\ndef test_get_item_from_value(client: TestClient, override: Overrider) -> None:\n override_item = Item(item_id=0, name=\"Bar\")\n override.value(lookup_item, override_item)\n\n response = client.get(\"/item/0\").json()\n\n assert Item(**response) == override_item\n```\n\nAlternatively use it as a context manager:\n\n```python\ndef test_get_item_context_manager(client: TestClient, app: FastAPI) -> None:\n with Overrider(app) as override:\n override_item = Item(item_id=0, name=\"Bar\")\n override.value(lookup_item, override_item)\n\n response = client.get(\"/item/0\").json()\n\n assert Item(**response) == override_item\n```\n\nIn both cases the overrides will be cleaned up after the test.\n\nThe above examples also show how to override a dependency with just the desired return\nvalue. Overrider will take care of creating a matching wrapper function and setting it\nas an override.\n\nIt doesn't matter if your dependency is async or not. Overrider will do the right thing.\n\n### Basic overrides\n\n`override.value()` returns the override value:\n\n```python\ndef test_get_item_return_value(client: TestClient, override: Overrider) -> None:\n item = override.value(lookup_item, Item(item_id=0, name=\"Bar\"))\n\n response = client.get(\"/item/0\").json()\n\n assert Item(**response) == item\n```\n\n`override.function()` accepts a callable:\n\n```python\ndef test_get_item_function(client: TestClient, override: Overrider) -> None:\n item = Item(item_id=0, name=\"Bar\")\n override.function(lookup_item, lambda item_id: item) # noqa: ARG005\n\n response = client.get(\"/item/0\").json()\n\n assert Item(**response) == item\n```\n\nUse it as a drop-in replacement for `app.dependency_overrides`:\n\n```python\ndef test_get_item_drop_in(client: TestClient, override: Overrider) -> None:\n item = Item(item_id=0, name=\"Bar\")\n def override_lookup_item(item_id: int) -> Item: # noqa: ARG001\n return item\n override[lookup_item] = override_lookup_item\n\n response = client.get(\"/item/0\").json()\n\n assert Item(**response) == item\n```\n\n### Mocks and spies\n\nOverrider can create mocks for you:\n\n```python\ndef test_get_item_mock(client: TestClient, override: Overrider) -> None:\n item = Item(item_id=0, name=\"Bar\")\n mock_lookup = override.mock(lookup_item)\n mock_lookup.return_value = item\n\n response = client.get(\"/item/0\")\n\n mock_lookup.assert_called_once_with(item_id=0)\n assert Item(**response.json()) == item\n```\n\nSpy on a dependency. The original dependency will still be called, but you can call assertions\nand inspect it like a `unittest.mock.Mock`:\n\n```python\ndef test_get_item_spy(client: TestClient, override: Overrider) -> None:\n spy = override.spy(lookup_item)\n\n client.get(\"/item/0\")\n\n spy.assert_called_with(item_id=0)\n```\n\n### Auto-generated overrides\n\nOverrider can auto-generate mock objects using [Unifactory](https://github.com/phha/unifactory).\n\nTo enable this extra feature, use `pip install fastapi-overrider[unifactory]`.\n\nOverrider will automatically use a matching factory from\n[Polyfactory's inventory](https://polyfactory.litestar.dev/usage/library_factories/index.html)\nfor the given dependency.\n\nGenerate a single override value. You can provide optional keyword arguments to any of the\nauto-generator methods in order to pin an attribute to a specific value, like `name` in\nthis example:\n\n```python\ndef test_get_some_item(client: TestClient, override: Overrider) -> None:\n item = override.some(lookup_item, name=\"Foo\")\n\n response = client.get(f\"/item/{item.item_id}\")\n\n assert item.name == \"Foo\"\n assert item == Item(**response.json())\n```\n\nYou can also let Overrider generate multiple override values:\n\n```python\ndef test_get_five_items(client: TestClient, override: Overrider) -> None:\n items = override.batch(lookup_item, 5)\n\n for item in items:\n response = client.get(f\"/item/{item.item_id}\")\n assert item == Item(**response.json())\n```\n\nAttempt to cover the full range of forms that a model can take:\n\n```python\ndef test_cover_get_items(client: TestClient, override: Overrider) -> None:\n items = override.cover(lookup_item)\n\n for item in items:\n response = client.get(f\"/item/{item.item_id}\")\n assert item == Item(**response.json())\n```\n\n### Shortcuts\n\nYou can call Overrider directly and it will guess what you want to do:\n\nIf you pass in a callable, it will act like `override.function()`:\n\n```python\ndef test_get_item_call_callable(client: TestClient, override: Overrider) -> None:\n item = Item(item_id=0, name=\"Bar\")\n override(lookup_item, lambda item_id: item) # noqa: ARG005\n\n response = client.get(\"/item/0\").json()\n\n assert Item(**response) == item\n```\n\nIf you pass in a non-callable, it will act like `override.value()`:\n\n```python\ndef test_get_item_call_value(client: TestClient, override: Overrider) -> None:\n item = override(lookup_item, Item(item_id=0, name=\"Bar\"))\n\n response = client.get(\"/item/0\").json()\n\n assert Item(**response) == item\n```\n\nIf you don't pass in anything, it will create a mock:\n\n```python\ndef test_get_item_call_mock(client: TestClient, override: Overrider) -> None:\n item = Item(item_id=0, name=\"Bar\")\n mock_lookup = override(lookup_item)\n mock_lookup.return_value = item\n\n response = client.get(\"/item/0\")\n\n mock_lookup.assert_called_once_with(item_id=0)\n assert Item(**response.json()) == item\n```\n\n### Advanced patterns\n\nReuse common overrides. They are composable, you can have multiple:\n\n```python\n@pytest.fixture()\ndef as_dave(app: FastAPI) -> Iterator[Overrider]:\n with Overrider(app) as override:\n override(get_user, User(name=\"Dave\", authenticated=True))\n yield override\n\n@pytest.fixture()\ndef in_the_morning(app: FastAPI) -> Iterator[Overrider]:\n with Overrider(app) as override:\n override(get_time_of_day, \"morning\")\n yield override\n\ndef test_get_greeting(client: TestClient, as_dave: Overrider, in_the_morning: Overrider) -> None:\n response = client.get(\"/\")\n\n assert response.text == '\"Good morning, Dave.\"'\n```\n\nExtend it with your own convenience methods:\n\n```python\nclass MyOverrider(Overrider):\n def user(self, *, name: str, authenticated: bool = False) -> None:\n self(get_user, User(name=name, authenticated=authenticated))\n\n@pytest.fixture()\ndef override(app: FastAPI):\n with MyOverrider(app) as override:\n yield override\n\ndef test_open_pod_bay_doors(client: TestClient, my_override: MyOverrider) -> None:\n my_override.user(name=\"Dave\", authenticated=False)\n\n response = client.get(\"/open/pod_bay_doors\")\n\n assert response.text == \"\\\"I'm afraid I can't let you do that, Dave.\\\"\"\n```\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "FastAPI Dependency overrides made easy.",
"version": "0.7.2",
"project_urls": {
"Homepage": "https://github.com/phha/fastapi-overrider",
"Repository": "https://github.com/phha/fastapi-overrider"
},
"split_keywords": [
"fastapi",
"pytest"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "b302697151ab6b76bb2a339e1008ab36410bd9df22c677f803cee2658805c783",
"md5": "c778bc2f10459da6650cd391b8aa6a23",
"sha256": "1702dfb28d21f71656f03d29c2a18132d65787d0694a7691f36e9b5ffd95de45"
},
"downloads": -1,
"filename": "fastapi_overrider-0.7.2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "c778bc2f10459da6650cd391b8aa6a23",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.10,<4.0",
"size": 5949,
"upload_time": "2024-01-04T19:42:49",
"upload_time_iso_8601": "2024-01-04T19:42:49.099539Z",
"url": "https://files.pythonhosted.org/packages/b3/02/697151ab6b76bb2a339e1008ab36410bd9df22c677f803cee2658805c783/fastapi_overrider-0.7.2-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "5f85b9e8553c31abdff67f4d654a0056a082841be428b3249c4f7eec69f72507",
"md5": "5a792d88d5b03a30d26acb399c616a79",
"sha256": "4f9dede4ef76afc663f578b35f4536e3433e23a7779d0335445e1916f0dbf662"
},
"downloads": -1,
"filename": "fastapi_overrider-0.7.2.tar.gz",
"has_sig": false,
"md5_digest": "5a792d88d5b03a30d26acb399c616a79",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.10,<4.0",
"size": 5726,
"upload_time": "2024-01-04T19:42:51",
"upload_time_iso_8601": "2024-01-04T19:42:51.601348Z",
"url": "https://files.pythonhosted.org/packages/5f/85/b9e8553c31abdff67f4d654a0056a082841be428b3249c4f7eec69f72507/fastapi_overrider-0.7.2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-01-04 19:42:51",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "phha",
"github_project": "fastapi-overrider",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "fastapi-overrider"
}