11

I've got a simple FastAPI application and I'm trying to create tests with pytest for it.

My goal is to test how app behaves in case of different errors.

I've got a simple healthcheck route in my app:

from fastapi import APIRouter

router = APIRouter()


@router.get("/health")
async def health():
    return "It's working ✨"

Now in my pytest module I'm trying to patch above function so that it raises different errors. I'm using unittest.mock but I'm getting very strange behavior.

import pytest
from unittest import mock

from fastapi import HTTPException
from starlette.testclient import TestClient

import app.api.health
from app.main import app  # this is my application (FastAPI instance) with the `router` attached


@pytest.fixture()
def client():
    with TestClient(app) as test_client:
        yield test_client


def test_simple(client):
    def mock_health_function():
        raise HTTPException(status_code=400, detail='gibberish')

    with mock.patch('app.api.health.health', mock_health_function):
        response = client.get(HEALTHCHECK_PATH)

        with pytest.raises(HTTPException):  # this check passes successfully - my exception is raised
            app.api.health.health()

    assert response.status_code != 200  # this check does not pass. The original function was called as if nothing was patched

Despite the fact that the exact same function is called inside the test, API test client still calls the original function when I hit the endpoint.

Why does mock.patch not work properly when function is not called directly in the test?

Or maybe I should approach my problem in some different way?

umat
  • 607
  • 1
  • 13
  • 25
  • You have to patch the module that is imported by the sut - see [where to patch](https://docs.python.org/3/library/unittest.mock.html#id6) documentation. You are currently patching the function imported in your test instead. – MrBean Bremen Mar 25 '20 at 17:52
  • Can you show how you import the mocked function in your tested code? – MrBean Bremen Mar 25 '20 at 17:59
  • Just like in the question: I import whole module `import app.api.health` and then call function directly `app.api.health.health()`. (Sorry for that nesting). `health` function is not imported anywhere else, it is just registered in the FastAPI router - and then it is called by the framework. – umat Mar 25 '20 at 18:12
  • Hm, the patching actually looks correct in that case... – MrBean Bremen Mar 25 '20 at 18:20
  • It seems that fastAPI's decorator `@route.get(...)` stores reference to the original function. Mock patches the function, but it happens after the route is already registered and the patched object has different id reference. – umat Mar 25 '20 at 20:11
  • you are patching a coroutine (`async def ...`). `client` (the fastapi application) will call your `health()` endpoint asynchronously (with `await ...`). use [asynctest](https://asynctest.readthedocs.io/en/latest/) to mock coroutines – Thomasleveil Mar 29 '20 at 11:41
  • I have the same issue. Did you find a solution? – julste Jan 08 '21 at 08:32
  • sam issue, did anyone found a solution? – sg_sg94 Apr 12 '21 at 12:01

1 Answers1

11

You can use monkeypatch fixture to patch your function.

First pull out the code section you want to patch:

from fastapi import FastAPI

app = FastAPI()


def response():
    return "It's working ✨"


@app.get("/health")
async def health():
    return response()

Then use monkeypatch in your test

import pytest

from fastapi import HTTPException
from starlette.testclient import TestClient

from app import main


def mocked_response():
    raise HTTPException(status_code=400, detail='gibberish')


@pytest.fixture()
def client():
    from app.main import app

    with TestClient(app) as test_client:
        yield test_client


def test_simple(client, monkeypatch):

    monkeypatch.setattr(main, "response", mocked_response)
    resp = client.get("/health")
    assert resp.status_code == 400
    assert resp.json()["detail"] == "gibberish"

Another approach would be to use Dependencies, together with dependencies_overrides. This probably won't work for all scenarios but for your given use case it does.

from fastapi import FastAPI,  Depends

app = FastAPI()


def response():
    return "It's working ✨"


@app.get("/health")
async def health(resp=Depends(response)):
    return resp

In your test client you can now override the dependency like this:

import pytest

from fastapi import HTTPException
from starlette.testclient import TestClient

from app.main import response


def mocked_response():
    raise HTTPException(status_code=400, detail='gibberish')


@pytest.fixture()
def client():
    from app.main import app
    app.dependency_overrides[response] = mocked_response

    with TestClient(app) as test_client:
        yield test_client


def test_simple(client):

    resp = client.get("/health")

    assert resp.status_code == 400
    assert resp.json()["detail"] == "gibberish"

If you need to add arguments to your response function you could make use of the closure pattern

def response_closure():
    def response(arg):
        return arg
    return response


@app.get("/health")
async def health(resp=Depends(response_closure)):
    return resp("It's working ✨")
Thomas
  • 1,026
  • 7
  • 18