pythonpytestfastapipytest-mock

Why is this monkeypatch in pytest not working?


As I am switching from Django to FastAPI, I also need to change tests from unittests to pytests. I build a custom TestAPI class and have test cases as methods, which works fine. However I want to override some functions (not dependencies) which are used in the code in one testcase. I tried this, but it doesn't work:

def test_smoke_me_api(self, monkeypatch):
    monkeypatch.setattr("app.auth.utils.get_user", mock_get_user)

    re = self.c.get("/me/")

It doesn't call the mock_get_user function, but instead the get_user one. According to some docs, I added the monkeypatch to the setup_class function of my test class, but this didn't work as this is apparently initialized with one argument only (self).

self.c is a client, which is a TestClient initialized in the setup_class.

Minimal example:

app/auth/utils.py

def get_user(sub) -> dict:
    re = requests.get(f"https://{API_DOMAIN}/api/v2/users/{sub}")
    return re.json()

app/auth/views.py

from app.auth.utils import get_user
@router.get("/")
async def me_get(sub: str = Security(auth.verify)) -> dict:
    return get_user(sub)

app/test_main.py

def mock_get_user(sub = "testing") -> dict:
    return {
        "created_at": "2023-08-15T13:25:31.507Z",
        "email": "test@test.org"
    }

class TestAPI:
    def setup_class(self):
        from app.main import app
        self.c = TestClient(app)

    def test_smoke_me_api(self, monkeypatch):
        monkeypatch.setattr("app.auth.utils.get_user", mock_get_user)
        re = self.c.get("/me/")

Solution

  • When in app.auth.views you run from app.auth.utils import get_user, you're creating a new reference to the original function get_user inside the app.auth.views module.

    Patching app.auth.utils.get_user changes the reference in app.auth.utils, but it leaves app.auth.views with the copy it previously made.

    There are two general approaches to fixing this:

    1. import the module, not the function.

      That is to say, do something more like import app.auth.utils as auth_utils, then call auth_utils.get_user() to refer back through the namespace to which you're applying the monkeypatch

    2. Monkeypatch the destination, not the source

      That is to say, instead of monkeypatching app.auth.utils.get_user, monkeypatch app.auth.views.get_user().