1

I'm testing a FastAPI application with asynchronous endpoints using pytest and httpx.AsyncClient. I have an endpoint /logout that depends on a JWT token from cookies and uses some async dependencies.


Here is my test:

import pytest
from httpx import ASGITransport, AsyncClient
from unittest.mock import AsyncMock

from app.main import app 


@pytest.mark.asyncio
async def test_auth_callback_creates_user(monkeypatch):
    mock_user = AsyncMock()
    mock_user.id = 123

    async def mock_authorize_access_token(request):
        return {"userinfo": {"email": "[email protected]", "given_name": "Test", "sub": "xyz"}}

    monkeypatch.setattr("app.routers.view.core_views.google.authorize_access_token", mock_authorize_access_token)
    monkeypatch.setattr("app.routers.view.core_views.get_user_email", AsyncMock(return_value=None))
    monkeypatch.setattr("app.routers.view.core_views.add_user", AsyncMock(return_value=mock_user))
    monkeypatch.setattr("app.routers.view.core_views.encode_jwt", lambda payload: "fake-jwt")
    monkeypatch.setattr("app.routers.view.core_views.save_jwt_token", AsyncMock())

    async with AsyncClient(
        transport=ASGITransport(app=app),
        base_url="http://test"
    ) as ac:
        resp = await ac.get("/auth/callback")

    assert resp.status_code == 302
    assert resp.headers["location"] == "/profile-view/"
    assert "access_token" in resp.cookies


@pytest.mark.asyncio
async def test_user_fixture(monkeypatch):
    mock_user = AsyncMock()
    mock_user.id = 123
    assert mock_user.id is not None


@pytest.mark.asyncio
async def test_logout_removes_cookie(monkeypatch):
    mock_payload = {"sub": "1"}
    monkeypatch.setattr("app.routers.view.core_views.get_current_token_payload", AsyncMock(return_value=mock_payload))
    monkeypatch.setattr("app.routers.view.core_views.blacklist_token", AsyncMock())

    async with AsyncClient(
        transport=ASGITransport(app=app),
        base_url="http://test"
    ) as ac:
        resp = await ac.get("/logout", cookies={"access_token": "fake-token"})

    assert resp.status_code == 302
    assert resp.headers["location"] == "/authentication"

command: poetry run pytest -v tests/test_user_oauth.py

requirements.txt: (necessary)

fastapi==0.112.4
httpx==0.28.1
pytest==8.3.4
pytest-asyncio==1.0.0
pytest-mock==3.14.1
SQLAlchemy==2.0.36
asyncpg==0.30.0
alembic==1.14.0
Authlib==1.4.0
python-dotenv==1.0.1
redis==6.2.0
PyJWT==2.10.1

I want to test that /logout returns a 302 redirect and removes the cookie. But instead I get an error. FAILED tests/test_user_oauth.py::test_logout_removes_cookie - RuntimeError: Event loop is closed

What I've tried:

  1. Putting the ASGITransport into pytest fixtures
  2. Explicitly closing the client with await ac.aclose() at the end of the test.
  3. Switching pytest.mark.asyncio to pytest.mark.anyio with anyio_backend = asyncio.
  4. looked at other posts on this topic.
2

1 Answer 1

1

The problem was caused by the event loop being closed between async tests. pytest-asyncio creates and destroys an event loop per test by default (scope = function).

That’s why I was getting:

RuntimeError: Event loop is closed


I fixed it by extending the event loop lifetime with:

@pytest.mark.asyncio(loop_scope="session")


Example:

@pytest.mark.asyncio(loop_scope="session")  # keeps one event loop for all async tests
async def test_eat_cookie():
    ...
# other async tests

Result: All tests passed, no more RuntimeError: Event loop is closed.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.