Pytest-django: Tests utilisant live_server fixture supprimant les données des migrations de données

Créé le 14 avr. 2016  ·  21Commentaires  ·  Source: pytest-dev/pytest-django

J'ai créé un cas de test simple pour reproduire ce comportement https://github.com/ekiro/case_pytest/blob/master/app/tests.py qui échoue après le deuxième test en utilisant live_server fixture.
Les objets MyModel sont créés lors de la migration, à l'aide de RunPython. Il semble qu'après tout test avec live_server, chaque ligne de la base de données soit tronquée. Postgresql et sqlite3 ont tous deux été testés.

ÉDITER:
Essais

"""
MyModel objects are created in migration
Test results:
    app/tests.py::test_no_live_server PASSED
    app/tests.py::test_live_server PASSED
    app/tests.py::test_live_server2 FAILED
    app/tests.py::test_no_live_server_after_live_server FAILED
"""

import pytest

from .models import MyModel


@pytest.mark.django_db()
def test_no_live_server():
    """Passed"""
    assert MyModel.objects.count() == 10


@pytest.mark.django_db()
def test_live_server(live_server):
    """Passed"""
    assert MyModel.objects.count() == 10


@pytest.mark.django_db()
def test_live_server2(live_server):
    """Failed, because count() returns 0"""
    assert MyModel.objects.count() == 10


@pytest.mark.django_db()
def test_no_live_server_after_live_server():
    """Failed, because count() returns 0"""
    assert MyModel.objects.count() == 10

Commentaire le plus utile

Je viens de le frapper aussi et il m'a fallu beaucoup de temps pour le retrouver, même si je suis assez familier avec le framework de test et les migrations de Django.

Il s'agit en fait d'un compromis de performance surprenant de la TestCase de Django. C'est documenté ici :
https://docs.djangoproject.com/en/1.9/topics/testing/overview/#rollback -emulation

Dans une suite de tests Django classique, la solution de contournement consiste à définir un attribut de classe serialized_rollback = True sur le cas de test.

Je ne sais pas comment obtenir le même effet avec les classes de test générées dynamiquement par pytest-django.

Tous les 21 commentaires

C'est à cause du fait que le projecteur transactional_db est utilisé automatiquement par live_server (il n'est pas nécessaire de le marquer avec @pytest.mark.django_db ). Il y a plusieurs problèmes/discussions à cet égard, mais la dernière fois que j'y ai regardé de plus près, il n'y avait pas de solution facile à ce problème lié à l'utilisation des migrations de données.
Une solution de contournement consiste à utiliser des appareils pytest pour envelopper vos migrations de données / appareils de données.
(Au fait : il vaut mieux mettre les tests en ligne dans le problème plutôt que dans une ressource externe qui pourrait changer avec le temps.)

Je viens de le frapper aussi et il m'a fallu beaucoup de temps pour le retrouver, même si je suis assez familier avec le framework de test et les migrations de Django.

Il s'agit en fait d'un compromis de performance surprenant de la TestCase de Django. C'est documenté ici :
https://docs.djangoproject.com/en/1.9/topics/testing/overview/#rollback -emulation

Dans une suite de tests Django classique, la solution de contournement consiste à définir un attribut de classe serialized_rollback = True sur le cas de test.

Je ne sais pas comment obtenir le même effet avec les classes de test générées dynamiquement par pytest-django.

Le changement suivant « résout » le problème au détriment de la sélection inconditionnelle du comportement le moins efficace.

--- pytest_django/fixtures.py.orig  2016-04-27 17:12:25.000000000 +0200
+++ pytest_django/fixtures.py   2016-04-27 17:21:50.000000000 +0200
@@ -103,6 +103,7 @@

     if django_case:
         case = django_case(methodName='__init__')
+        case.serialized_rollback = True
         case._pre_setup()
         request.addfinalizer(case._post_teardown)

La technique suivante fonctionne, mais je ne peux pas la recommander pour des raisons assez évidentes...

import pytest
from django.core.management import call_command
from pytest_django.fixtures import transactional_db as _transactional_db


def _reload_fixture_data():
    fixture_names = [
        # Create fixtures for the data created by data migrations
        # and list them here.
    ]
    call_command('loaddata', *fixture_names)


@pytest.fixture(scope='function')
def transactional_db(request, _django_db_setup, _django_cursor_wrapper):
    """
    Override a pytest-django fixture to restore the contents of the database.

    This works around https://github.com/pytest-dev/pytest-django/issues/329 by
    restoring data created by data migrations. We know what data matters and we
    maintain it in (Django) fixtures. We don't read it from the database. This
    causes some repetition but keeps this (pytest) fixture (almost) simple.

    """
    try:
        return _transactional_db(request, _django_db_setup, _django_cursor_wrapper)
    finally:
        # /!\ Epically shameful hack /!\ _transactional_db adds two finalizers:
        # _django_cursor_wrapper.disable() and case._post_teardown(). Note that
        # finalizers run in the opposite order of that in which they are added.
        # We want to run after case._post_teardown() which flushes the database
        # but before _django_cursor_wrapper.disable() which prevents further
        # database queries. Hence, open heart surgery in pytest internals...
        finalizers = request._fixturedef._finalizer
        assert len(finalizers) == 2
        assert finalizers[0].__qualname__ == 'CursorManager.disable'
        assert finalizers[1].__qualname__ == 'TransactionTestCase._post_teardown'
        finalizers.insert(1, _reload_fixture_data)

Une option d'activation conditionnelle de serialized_rollback serait-elle une bonne solution ?

C'est-à-dire quelque chose comme

@pytest.mark.django_db(transaction=True, serialized_rollback=True)
def test_foo():
    ...

Je pense que ce serait bien.

Dans mon cas d'utilisation :

  • l'appareil transactional_db est déclenché par live_server , mais cela ne me dérangerait pas d'ajouter un appareil django_db pour spécifier un rollback sérialisé
  • sans rapport avec pytest-django -- case.serialized_rollback = True échoue toujours car Django tente de désérialiser les objets dans un ordre qui ne respecte pas les contraintes FK, je pense que c'est un bogue dans Django

OK parfait :)

Je n'ai pas le temps de travailler sur un correctif pour le moment, mais ajouter une telle option au marqueur django_db et définir case.serialized_rollback = True (comme vous l'avez fait ci-dessus) devrait être relativement simple.

Ticket pour le bug Django que j'ai mentionné ci-dessus : https://code.djangoproject.com/ticket/26552

@pelme -- cette pull request implémente-t-elle l'approche "relativement simple" que vous envisagez ?

Merci beaucoup pour le PR. La réalisation a l'air correcte et conforme à ce que j'imaginais. Je n'avais pas pensé à avoir un fixture serialized_rollback mais il est effectivement utile de pouvoir forcer ce comportement depuis d'autres fixtures.

Nous aurions besoin d'un test pour cela aussi, mais l'implémentation semble correcte !

Je vais essayer de trouver le temps de peaufiner le patch (je ne l'ai même pas encore lancé). Je dois également corriger le bogue mentionné dans Django avant de pouvoir l'utiliser.

Version mise à jour du hack ci-dessus pour pytest-django ≥ 3.0 :

@pytest.fixture(scope='function')
def transactional_db(request, django_db_setup, django_db_blocker):
    """
    Override a pytest-django fixture to restore the contents of the database.

    This works around https://github.com/pytest-dev/pytest-django/issues/329 by
    restoring data created by data migrations. We know what data matters and we
    maintain it in (Django) fixtures. We don't read it from the database. This
    causes some repetition but keeps this (pytest) fixture (almost) simple.

    """
    try:
        return _transactional_db(request, django_db_setup, django_db_blocker)
    finally:
        # /!\ Epically shameful hack /!\ _transactional_db adds two finalizers:
        # django_db_blocker.restore() and test_case._post_teardown(). Note that
        # finalizers run in the opposite order of that in which they are added.
        # We want to run after test_case._post_teardown() flushes the database
        # but before django_db_blocker.restore() prevents further database
        # queries. Hence, open heart surgery in pytest internals...
        finalizers = request._fixturedef._finalizer
        assert len(finalizers) == 2
        assert finalizers[0].__qualname__ == '_DatabaseBlocker.restore'
        assert finalizers[1].__qualname__ == 'TransactionTestCase._post_teardown'
        finalizers.insert(1, _reload_fixture_data)

Merci pour la mise à jour et désolé de ne pas avoir pu l'intégrer à la 3.0. Je vais essayer d'y arriver pour la prochaine version !

Y a-t-il une chance de le voir dans la prochaine version?

Merci

@Wierrat
Oui probablement.

Pouvez-vous nous aider avec un test pour https://github.com/pytest-dev/pytest-django/pull/353 avant cela, s'il vous plaît ?

Salut les gens,
quel est le dernier statut de ceci? En particulier, j'utilise pytest comme lanceur de test avec la classe StaticLiveServerTestCase de Django. J'ai défini l'attribut de classe serialized_rollback = True mais cela semble être en vigueur uniquement lors de l'exécution du premier test de la séquence.

Je viens de me faire attraper par celui-ci. En regardant autour, il semble qu'il y ait eu des relations publiques sur cette question, mais aucune d'entre elles ne semble avoir été fusionnée.
Y a-t-il une raison particulière pour laquelle ce problème reste ouvert ?

quelques relations publiques

Probable/uniquement https://github.com/pytest-dev/pytest-django/issues/329 , non ?

La dernière fois que j'ai demandé de l'aide pour un test, il y a plusieurs conflits (peut-être uniquement à cause du noir).

Je suggère toujours à toute personne concernée d'aider avec cela - je ne l'utilise pas moi-même.

Merci pour la réponse rapide @blueyed.
Ma connaissance de pytest / pytest-django est minime mais je vais mettre ça sur ma liste somday/peut-être ! :RÉ

Version mise à jour du hack ci-dessus pour pytest 5.2.1, pytest-django 3.5.1 :

from functools import partial

@pytest.fixture
def transactional_db(request, transactional_db, django_db_blocker):
    # Restore DB content after all of transactional_db's finalizers have
    # run. Finalizers are run in the opposite order of that in which they
    # are added, so we prepend the restore to the front of the list.
    #
    # Works for pytest 5.2.1, pytest-django 3.5.1
    restore = partial(_restore_db_content, django_db_blocker)
    finalizers = request._fixture_defs['transactional_db']._finalizers
    finalizers.insert(0, restore)

    # Simply restoring after yielding transactional_db wouldn't work because
    # it would run before transactional_db's finalizers which contains the truncate.
    return transactional_db

def _restore_db_content(django_db_fixture, django_db_blocker):
    with django_db_blocker.unblock():
        call_command('loaddata', '--verbosity', '0', 'TODO your fixture')

Je devais dépendre de l'appareil d'origine transactional_db au lieu de l'appeler car pytest ne permet plus d'appeler directement les fonctions d'appareil. J'obtiens ensuite la définition de l'appareil d'origine et j'y ajoute un finaliseur. Alors que l'insertion à l'index 1 fonctionnait toujours, j'insère avant tous les finaliseurs et utilise django_db_blocker dans la fonction de restauration car cela semble légèrement moins fragile.

Edit : supprimé l'essai inutile finalement.

@lukaszb

https://github.com/pytest-dev/pytest-django/issues/848

J'utilise cette version de pytest-django (https://github.com/pytest-dev/pytest-django/pull/721/files),
Mais erreur quand même.

image

Cette page vous a été utile?
0 / 5 - 0 notes