Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

FastAPI - mocking path function has no effect

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?

like image 629
umat Avatar asked Mar 25 '20 17:03

umat


1 Answers

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 ✨")
like image 131
Thomas Avatar answered Sep 20 '22 12:09

Thomas