简体   繁体   中英

RecursionError when mocking function by FastAPI-Testclient

I would like to test the interplay with two FastAPIs using python 3.8 . In the running code, one API ( main_app ) is calling the other API ( helper_app ) by the function connect_to_helper_app . To test this without setting up two servers, I would like to use fastapi.testclient.TestClient . Unfortunately, I get a RecursionError .

To reproduce the error, you need the following files:

# Content of minimal_example/apps.py

from fastapi import FastAPI

main_app = FastAPI()
helper_app = FastAPI()


def connect_to_helper_app():
    """
    This function will be mocked in the test. In the real code, a request to the other app would be made
    """
    raise NotImplemented


@main_app.get("/call_helper")
def call_helper() -> dict:
    return connect_to_helper_app()


@helper_app.get("/")
def root() -> dict:
    return {"msg": "This is the helper app."}

And following test file:

# Content of minimal_example/test.py

from fastapi.testclient import TestClient
from minimal_example.apps import main_app, helper_app

helper_test_client = TestClient(helper_app)
main_test_client = TestClient(main_app)


def test(mocker):
    def connect_to_helper_app_with_test_client():
        result = helper_test_client.get('/')
        return result
    mocker.patch('minimal_example.apps.connect_to_helper_app', new=connect_to_helper_app_with_test_client)
    main_test_client.get('/call_helper')

The error message I get is:

test.py:7 (test)
mocker = <pytest_mock.plugin.MockerFixture object at 0x0000015DFFB93F40>

    def test(mocker):
        def connect_to_helper_app_with_test_client():
            result = helper_test_client.get('/')
            return result
        mocker.patch('minimal_example.apps.connect_to_helper_app', new=connect_to_helper_app_with_test_client)
    
>       main_test_client.get('/call_helper')

test.py:14: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
..\..\.venv\lib\site-packages\requests\sessions.py:542: in get
    return self.request('GET', url, **kwargs)
..\..\.venv\lib\site-packages\starlette\testclient.py:415: in request
    return super().request(
..\..\.venv\lib\site-packages\requests\sessions.py:529: in request
    resp = self.send(prep, **send_kwargs)
..\..\.venv\lib\site-packages\requests\sessions.py:645: in send
    r = adapter.send(request, **kwargs)
..\..\.venv\lib\site-packages\starlette\testclient.py:243: in send
    raise exc from None
..\..\.venv\lib\site-packages\starlette\testclient.py:240: in send
    loop.run_until_complete(self.app(scope, receive, send))
C:\Users\d91802\AppData\Local\Programs\Python\Python38\lib\asyncio\base_events.py:616: in run_until_complete
    return future.result()
..\..\.venv\lib\site-packages\fastapi\applications.py:208: in __call__
    await super().__call__(scope, receive, send)
..\..\.venv\lib\site-packages\starlette\applications.py:112: in __call__
    await self.middleware_stack(scope, receive, send)
..\..\.venv\lib\site-packages\starlette\middleware\errors.py:181: in __call__
    raise exc from None
..\..\.venv\lib\site-packages\starlette\middleware\errors.py:159: in __call__
    await self.app(scope, receive, _send)
..\..\.venv\lib\site-packages\starlette\exceptions.py:82: in __call__
    raise exc from None
..\..\.venv\lib\site-packages\starlette\exceptions.py:71: in __call__
    await self.app(scope, receive, sender)
..\..\.venv\lib\site-packages\starlette\routing.py:580: in __call__
    await route.handle(scope, receive, send)
..\..\.venv\lib\site-packages\starlette\routing.py:241: in handle
    await self.app(scope, receive, send)
..\..\.venv\lib\site-packages\starlette\routing.py:52: in app
    response = await func(request)
..\..\.venv\lib\site-packages\fastapi\routing.py:234: in app
    response_data = await serialize_response(
..\..\.venv\lib\site-packages\fastapi\routing.py:148: in serialize_response
    return jsonable_encoder(response_content)
..\..\.venv\lib\site-packages\fastapi\encoders.py:145: in jsonable_encoder
    return jsonable_encoder(
..\..\.venv\lib\site-packages\fastapi\encoders.py:93: in jsonable_encoder
    encoded_value = jsonable_encoder(
..\..\.venv\lib\site-packages\fastapi\encoders.py:145: in jsonable_encoder
    return jsonable_encoder(
..\..\.venv\lib\site-packages\fastapi\encoders.py:93: in jsonable_encoder
    encoded_value = jsonable_encoder(
..\..\.venv\lib\site-packages\fastapi\encoders.py:145: in jsonable_encoder
    return jsonable_encoder(
..\..\.venv\lib\site-packages\fastapi\encoders.py:93: in jsonable_encoder
    encoded_value = jsonable_encoder(
..\..\.venv\lib\site-packages\fastapi\encoders.py:145: in jsonable_encoder
    return jsonable_encoder(
..\..\.venv\lib\site-packages\fastapi\encoders.py:93: in jsonable_encoder
    encoded_value = jsonable_encoder(
..\..\.venv\lib\site-packages\fastapi\encoders.py:107: in jsonable_encoder
    jsonable_encoder(
..\..\.venv\lib\site-packages\fastapi\encoders.py:145: in jsonable_encoder
    return jsonable_encoder(
..\..\.venv\lib\site-packages\fastapi\encoders.py:93: in jsonable_encoder
    encoded_value = jsonable_encoder(
..\..\.venv\lib\site-packages\fastapi\encoders.py:145: in jsonable_encoder
    return jsonable_encoder(
..\..\.venv\lib\site-packages\fastapi\encoders.py:93: in jsonable_encoder
    encoded_value = jsonable_encoder(
E   RecursionError: maximum recursion depth exceeded in comparison
!!! Recursion detected (same locals & position)

In the debugger, I see that the function is sucessfully mocked by connect_to_helper_app_with_test_client and within that mocked function, the call to the helper_app returns the expected value ( {"msg": "This is the helper app."} ).

I would like to understand where the recursion is coming from, and how to avoid the error. Thanks in advance!

I think you should pull your connect_to_helper_app() into a separate class. This way you can mock the import instead of the actual implementation.

# Content of minimal_example/connection.py
def connect_to_helper_app():
    """
    This function will be mocked in the test. In the real code, a request to the other app would be made
    """
    raise NotImplemented

# Content of minimal_example/apps.py

from fastapi import FastAPI
from minimal_example.connection import connect_to_helper_app

main_app = FastAPI()
helper_app = FastAPI()


@main_app.get("/call_helper")
def call_helper() -> dict:
    return connect_to_helper_app()


@helper_app.get("/")
def root() -> dict:
    return {"msg": "This is the helper app."}
# Content of minimal_example/test.py

from fastapi.testclient import TestClient
from minimal_example.apps import main_app, helper_app

helper_test_client = TestClient(helper_app)
main_test_client = TestClient(main_app)


def test(mocker):
    def connect_to_helper_app_with_test_client():
        result = helper_test_client.get('/')
        return result
    mocker.patch('minimal_example.apps.connect_to_helper_app', new=connect_to_helper_app_with_test_client)
    main_test_client.get('/call_helper')

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM