Python FASTAPI Testing

From bibbleWiki
Revision as of 20:44, 4 June 2026 by Iwiseman (talk | contribs) (Replacing Dependencies)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Introduction

This page is to capture how to go about testing with FAST API

MonkeyPatch

So you can patch code for testing with monkey patch

monkeypatch.setattr("module.path.name", replacement)

Patch a Function

So you can

monkeypatch.setattr("module.path.get_user", lambda id: fake_user)

Which is like

jest.mock("./getUser", () => () => fakeUser)

Patch a module‑level variable

Setting a variable

monkeypatch.setattr(settings, "API_KEY", "test-key")

Which is like

jest.mock("./settings", () => ({ API_KEY: "test-key" }))

Patch an imported reference

If a module does

from server.encryptors.factory import get_encryptor

Then you can patch it like this

monkeypatch.setattr("server.module.that.uses.it.get_encryptor", fake)

Comparison to Jest

Here is a table to help compare terminology

Concept Jest pytest
Test setup beforeEach, beforeAll Fixtures (function, module, session scope)
Test teardown afterEach, afterAll Fixture finalizers / yield fixtures
Dependency injection Manual mocks, passing objects Fixture injection by name
Auto‑applied setup Global beforeEach in setup files @pytest.fixture(autouse=True)
Mocking jest.mock, jest.spyOn monkeypatch.setattr, unittest.mock.patch
Test data factories Helper functions Fixtures returning objects

Fixtures

These are things you create to set up a test. The autouse means you do not need to pass the fixture to the tests.

#  Force argon2 for testing purposes
@pytest.fixture(autouse=True)
def force_argon2(monkeypatch: pytest.MonkeyPatch):
    monkeypatch.setattr(settings, "authentication_encryptor", EncryptorNames.ARGON2.value)
    monkeypatch.setattr(user_management.password_manager, "encryptor", get_encryptor(EncryptorNames.ARGON2))

The first sets variable authentication_encryptor in the module settings to the Enum EncryptorNames.ARGON2.value

The second sets the class property encryptor for module user_management.password_manager to the result of get_encryptor(EncryptorNames.ARGON2).

@patch

You can replace a function during a test using @patch. This does not change the function everywhere — it only replaces the name inside the module you specify.

In this example, any call to `_get_msal_app` inside `server.routers.ms_auth` will raise `ConfigurationError("bad config")` instead of running the real function.

@patch(
    "server.routers.ms_auth._get_msal_app",
    side_effect=ConfigurationError("bad config")
)
async def should_redirect_on_msal_config_error(
    self,
    _mock_get_app,   # injected mock (unused)
    test_client
):
    response = await test_client.get("/api/ms/authorize")

The parameter `_mock_get_app` is required because @patch injects the mock into the test function. Even if you don't use it, the test must accept it.

Replacing Dependencies

FastAPI allows you to replace dependencies during testing. This is done using a special dictionary on the FastAPI app:

app.dependency_overrides[original_dependency] = replacement_dependency

FastAPI checks this dictionary at request time. If an override exists, FastAPI calls the replacement instead of the real dependency.

This lets you “fake” authentication, database access, or any other dependency.

Example:

@pytest.fixture(autouse=True)
def override_thing_dependency():
    def fake_thing():
        class FakeThing:
            def skip_thing_function(self):
                pass
        return FakeThing()

    # Replace the real dependency with our fake version
    app.dependency_overrides[thing_injected] = fake_thing

    # After the test, restore the original dependency
    yield
    app.dependency_overrides.pop(thing_injected, None)

This ensures every test receives the fake dependency instead of the real one.

Fixtures and Yield

If you use fixtures with yield pytest re-implements yield and adds a try, finally around the yield to ensure the takedown occurs.

import pytest

state = {"cleaned": False}

@pytest.fixture
def f():
    print("SETUP")
# Implemented by pytest
# try:
    yield
# finally
    print("TEARDOWN")
    state["cleaned"] = True

def test_fail(f):
    print("TEST BODY")
    raise RuntimeError("boom")