Les Crash-Devs d'un Passionné

Modifier le scope d'une fixture dans une event loop

/Catégorie/python

Temps de lecture : 5 minutes

Lors que vous avez une fonction de test qui utilise

asyncio
avec un appel d'une fixture vous rencontrez un résultat valide une fois sur deux

tests_failed_1_in_2_times

Avec comme message

got Future <Future pending> attached to a different loop

Bien évidemment de manière unitaire les tests passent avec succès.

Je vais vous expliquer comment résoudre ce problème et quelle est la meilleure solution.

Revenons un peu sur le message

different loop
: une
loop
est plus exactement une
event loop
est un mécanisme asynchrone permettant de mettre en place une gestion des évènements dans le programme. Un évènement est simplement un bout de code à exécuter.

Le plugin pytest-asyncio qui est installé dans votre projet attache votre test à l'event loop qu'

asyncio
a créé pour nous dans l'environnement de test

clipboard
Copier le code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# conftest.py
@pytest_asyncio.fixture(name="database", scope="session")
async def fixture_database():
    await create_database()
    yield
    await cleanup_database()

# test_css.py
@pytest.mark.asyncio
async def test_css(database):
    assert True

De ce fait on identifie que la configuration de pytest-asyncio est défaillante car notre

assert True
devrait retourner un test valide au travers de la fixture
database

En effet par défaut ce plugin donne un scope

function
à la fixture
database
c'est à dire dès qu'un test est terminé il exécute l'autre test dans une autre event loop d'où le message
got Future <Future pending> attached to a different loop

Il faut bien distinguer le scope de la fixture (qui est la durée de vie de la fixture) avec le scope de l'event loop qui est son exécution dans la boucle évènementielle.

Il faut donc modifier le scope de l'event loop à un niveau supérieur à

function
, nous allons mettre
session
cela indique que la fixture sera attachée à l'event loop durant l'exécution de l'intégralité des tests.

Pour cela il suffit simplement d'ajouter l'option

scope_loop
sur le test

clipboard
Copier le code
1
2
3
@pytest.mark.asyncio(loop_scope="session")
async def test_css(database):
    assert True

Les tests passent maintenant mais vous obtenez un warning de ce type :

PytestDeprecationWarning: The configuration option "asyncio_default_fixture_loop_scope" is unset.
The event loop scope for asynchronous fixtures will default to the fixture caching scope. Future versions of pytest-asyncio will default the loop scope for asynchronous fixtures to function scope. Set the default fixture loop scope explicitly in order to avoid unexpected behavior in the future. Valid fixture loop scopes are: "function", "class", "module", "package", "session"

Il faut donc mettre en place cette configuration dans le fichier pyproject.toml

clipboard
Copier le code
1
2
3
4
# pyproject.toml
...
[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "session"

Et maintenant tout est rentré dans l'ordre tous les tests passent sans warning

Et si on veut ajouter l'option loop_scope par défaut ?

On peut imaginer ne pas vouloir toujours préciser le

loop_scope
à
session
, au travers le decorateur
pytest.mark.asyncio
, pour tous les tests qui en ont besoin. Ainsi on peut se connecter sur le hook de
pytest
pour l'injecter automatiquement

clipboard
Copier le code
1
2
3
4
5
6
7
# conftest.py
def pytest_collection_modifyitems(items):
    """https://pytest-asyncio.readthedocs.io/en/v0.24.0/how-to-guides/run_session_tests_in_same_loop.html"""
    pytest_asyncio_tests = (item for item in items if is_async_test(item))
    session_scope_marker = pytest.mark.asyncio(loop_scope="session")
    for async_test in pytest_asyncio_tests:
        async_test.add_marker(session_scope_marker, append=False)

Et maintenant on a juste à préciser le plugin

asyncio
sans l'option
loop_scope
pour qu'il soit injecté par
pytest
.

Il est important de mettre tout de même

@pytest.mark.asyncio
sur vos tests car ce décorateur permet de retourner cette fonction lorsque le hook
pytest
appelle
is_async_test()

Conlusion

  1. Je vous préconise de passer par le décorateur
    @pytest.mark.asyncio(loop_scope="session")
    sur vos fonctions de tests. Cette option permet de choisir directement sur le test le scope de l'event loop en fonction des usages de votre
    fixture
    . Par exemple sur un jeu de donnée pour un test d'intégration le scope de la
    fixture
    serait plutôt en
    fonction
    .
  2. Je recommande moins la modification en globale car le comportement est caché dans
     conftest.py
    et est moins visible en lisant les fonctions de test. Et une fois que vous appliquez ce hook la modification sera effectuée sur TOUTES les fonctions de test