Search code examples
pythonfastapihttpxpytest-asynciofastapiusers

"Runtime Error: Event loop is closed" during testing using Pytest


Please help me. How to fix it? The error appears only when using cookies in test_logout()

conftest.py
...............

@pytest_asyncio.fixture(autouse=True, scope='session')
async def prepare_database():
    async with engine_test.begin() as conn:
        await conn.run_sync(metadata.create_all)
    yield
    async with engine_test.begin() as conn:
        await conn.run_sync(metadata.drop_all)


@pytest.fixture(scope='session')
def event_loop(request):
    loop = asyncio.get_event_loop_policy().new_event_loop()
    yield loop
    loop.close()


client = TestClient(app)

...............

test_auth.py
import asyncio
import pytest
import pytest_asyncio

from conftest import client


def test_register():
    response = client.post("/auth/register", json={
        "email": "[email protected]",
        "password": "string",
        "is_active": True,
        "is_superuser": False,
        "is_verified": False
    })
    assert response.status_code == 201


def test_login():
    data = {
        "username": "[email protected]",
        "password": "string",
    }
    encoded_data = "&".join([f"{key}={value}" for key, value in data.items()])
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }
    response = client.post("/auth/login", data=encoded_data, headers=headers)
    assert response.status_code == 204


def test_logout():
    cookies = {k: client.cookies.get(k) for k in client.cookies}
    print(cookies)
    headers = {
        "Content-Type": "application/json"
    }
    response = client.post("/auth/logout", headers=headers, cookies=cookies)
    print(response)
    assert response.status_code == 201


Pytest:

platform linux -- Python 3.11.3, pytest-7.4.0, pluggy-1.2.0 -- /home/djamal/PycharmProjects/green/venv/bin/python cachedir: .pytest_cache rootdir: /home/djamal/PycharmProjects/green/server plugins: anyio-3.7.1, asyncio-0.21.0 asyncio: mode=Mode.STRICT collected 3 items

tests/test_auth.py::test_register PASSED [ 33%] tests/test_auth.py::test_login PASSED [ 66%] tests/test_auth.py::test_logout FAILED [100%]

FAILURES ======================================================================================== test_logout ______________________________________________________________________________________

self = Connection<host=localhost,port=6379,db=0>, disable_decoding = False, timeout = None

async def read_response( self, disable_decoding: bool = False, timeout: Optional[float] = None, *, disconnect_on_error: bool = True, ): """Read the response from a previously sent command""" read_timeout = timeout if timeout is not None else self.socket_timeout host_error = self._host_error() try: if read_timeout is not None: async with async_timeout(read_timeout): response = await self._parser.read_response( disable_decoding=disable_decoding ) else: response = await self._parser.read_response( disable_decoding=disable_decoding )

../venv/lib/python3.11/site-packages/redis/asyncio/connection.py:782: _ ../venv/lib/python3.11/site-packages/redis/asyncio/connection.py:262: in read_response response = await self._read_response(disable_decoding=disable_decoding) ../venv/lib/python3.11/site-packages/redis/asyncio/connection.py:270: in _read_response raw = await self._readline() ../venv/lib/python3.11/site-packages/redis/asyncio/connection.py:344: in _readline data = await self._stream.readline() /usr/lib/python3.11/asyncio/streams.py:545: in readline line = await self.readuntil(sep) /usr/lib/python3.11/asyncio/streams.py:637: in readuntil await self._wait_for_data('readuntil') _

self = <StreamReader transport=<_SelectorSocketTransport closing fd=18>>, func_name = 'readuntil'

async def _wait_for_data(self, func_name): """Wait until feed_data() or feed_eof() is called.

If stream was paused, automatically resume it. """ StreamReader uses a future to link the protocol feed_data() method to a read coroutine. Running two read coroutines at the same time would have an unexpected behaviour. It would not possible to know which coroutine would get the next data. if self._waiter is not None: raise RuntimeError( f'{func_name}() called while another coroutine is ' f'already waiting for incoming data')

assert not self._eof, '_wait_for_data after EOF'

Waiting for data while paused will make deadlock, so prevent it. This is essential for readexactly(n) for case when n > self._limit. if self._paused: self._paused = False self._transport.resume_reading()

self._waiter = self._loop.create_future() try: await self._waiter E RuntimeError: Task <Task pending name='anyio.from_thread.BlockingPortal._call_func' coro=<BlockingPortal._call_func() running at /home/djamal/PycharmProjects/green/venv/lib/python3.11/site-packages/anyio/from_thread.py:217> cb=[TaskGroup._spawn..task_done() at /home/djamal/PycharmProjects/green/venv/lib/python3.11/site-packages/anyio/_backends/_asyncio.py:661]> got Future attached to a different loop

/usr/lib/python3.11/asyncio/streams.py:522: RuntimeError

During handling of the above exception, another exception occurred:

def test_logout(): cookies = {k: client.cookies.get(k) for k in client.cookies} print(cookies) headers = { "Content-Type": "application/json" } response = client.post("/auth/logout", headers=headers, cookies=cookies)

tests/test_auth.py:38: _ .......................... _

self = <_UnixSelectorEventLoop running=False closed=True debug=False>

def _check_closed(self): if self._closed: raise RuntimeError('Event loop is closed') E RuntimeError: Event loop is closed

/usr/lib/python3.11/asyncio/base_events.py:519: RuntimeError Captured stdout call ---------------------------------------------------------------------------------- {'fastapiusersauth': 'X5LEEnDGUSGXIhA5gOTQJSyJQ0g7xsKXvx4v2xBFCv8'} short test summary info ================================================================================ FAILED tests/test_auth.py::test_logout - RuntimeError: Event loop is closed 1 failed, 2 passed in 1.00s ==============================================================================

I've been busy with this for 4 hours, ai gpt didn't help


Solution

  • You should create your client in a fixture, and set the scope to "module" or "session" to reuse it across tests.

    https://tonybaloney.github.io/posts/async-test-patterns-for-pytest-and-unittest.html

    @pytest_asyncio.fixture(scope="session", autouse=False)
    async def async_client():
        async with AsyncClient(app=app, base_url='http://test') as client:
            yield client
        await client.close()
    
    @pytest.mark.asyncio
    async def test_login(async_client):
        await async_client.post(...)
    
    @pytest.mark.asyncio
    async def test_logout(async_client):
        await async_client.post(...)