Pytest-django: Tests mit live_server Fixture Entfernen von Daten aus Datenmigrationen

Erstellt am 14. Apr. 2016  ·  21Kommentare  ·  Quelle: pytest-dev/pytest-django

Ich habe einen einfachen Testfall erstellt, um dieses Verhalten zu reproduzieren https://github.com/ekiro/case_pytest/blob/master/app/tests.py, der nach dem zweiten Test mit der live_server-Fixture fehlschlägt.
MyModel-Objekte werden bei der Migration mit RunPython erstellt. Es scheint, als ob nach jedem Test mit live_server jede Zeile aus der Datenbank abgeschnitten wird. Sowohl postgresql als auch sqlite3 wurden getestet.

BEARBEITEN:
Tests

"""
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

Hilfreichster Kommentar

Ich bin auch gerade darauf gestoßen und habe lange gebraucht, um es aufzuspüren, obwohl ich mit dem Testframework und den Migrationen von Django einigermaßen vertraut bin.

Dies ist eigentlich ein überraschender Performance-Kompromiss von Djangos TestCase . Es ist hier dokumentiert:
https://docs.djangoproject.com/en/1.9/topics/testing/overview/#rollback -emulation

In einer regulären Django-Testsuite besteht die Problemumgehung darin, ein Klassenattribut serialized_rollback = True für den Testfall festzulegen.

Ich weiß nicht, wie ich den gleichen Effekt mit den dynamisch generierten Testklassen von pytest-django

Alle 21 Kommentare

Das liegt daran, dass transactional_db automatisch von live_server (es muss nicht mit @pytest.mark.django_db markiert werden). Diesbezüglich gibt es mehrere Probleme/Diskussionen, aber als ich mir das letzte Mal genauer angesehen habe, gab es keine einfache Lösung für dieses Problem, das mit der Verwendung von Datenmigrationen einhergeht.
Eine Problemumgehung besteht darin, pytest-Fixtures zu verwenden, um Ihre Datenmigrationen / Datenfixierungen zu umschließen.
(Übrigens: Stellen Sie die Tests besser inline in das Issue ein, als in eine externe Ressource, die sich im Laufe der Zeit ändern könnte.)

Ich bin auch gerade darauf gestoßen und habe lange gebraucht, um es aufzuspüren, obwohl ich mit dem Testframework und den Migrationen von Django einigermaßen vertraut bin.

Dies ist eigentlich ein überraschender Performance-Kompromiss von Djangos TestCase . Es ist hier dokumentiert:
https://docs.djangoproject.com/en/1.9/topics/testing/overview/#rollback -emulation

In einer regulären Django-Testsuite besteht die Problemumgehung darin, ein Klassenattribut serialized_rollback = True für den Testfall festzulegen.

Ich weiß nicht, wie ich den gleichen Effekt mit den dynamisch generierten Testklassen von pytest-django

Die folgende Änderung "löst" das Problem auf Kosten der bedingungslosen Auswahl des am wenigsten effizienten Verhaltens.

--- 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)

Die folgende Technik funktioniert, aber ich kann sie aus ziemlich offensichtlichen Gründen nicht empfehlen...

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)

Wäre eine Option zum bedingten Aktivieren von serialized_rollback eine gute Lösung?

Dh sowas wie

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

Ich denke, das wäre gut.

In meinem Anwendungsfall:

  • das transactional_db Fixture wird von live_server ausgelöst, aber ich hätte nichts dagegen, ein django_db Fixture hinzuzufügen, um ein serialisiertes Rollback zu spezifizieren
  • unabhängig von pytest-django -- case.serialized_rollback = True schlägt immer noch fehl, weil Django versucht, Objekte in einer Reihenfolge zu deserialisieren, die die FK-Einschränkungen nicht respektiert. Ich denke, das ist ein Fehler in Django

OK perfekt :)

Ich habe gerade keine Zeit, an einem Fix zu arbeiten, aber das Hinzufügen einer solchen Option zum Marker django_db und das Setzen von case.serialized_rollback = True (wie oben) sollte relativ einfach sein.

Ticket für den oben erwähnten Django-Bug: https://code.djangoproject.com/ticket/26552

@pelme - implementiert diese Pull-Anfrage den "relativ einfachen" Ansatz, den Sie sich vorstellen?

Vielen Dank für die PR. Die Umsetzung sieht richtig aus und so wie ich es mir vorgestellt habe. Ich habe nicht daran gedacht, ein serialized_rollback Fixture zu haben, aber es ist in der Tat nützlich, dieses Verhalten von anderen Fixtures erzwingen zu können.

Auch hierfür bräuchten wir einen Test, aber die Umsetzung sieht richtig aus!

Ich werde versuchen, Zeit zu finden, um den Patch zu polieren (ich habe ihn noch nicht einmal ausgeführt). Ich muss auch den erwähnten Fehler in Django beheben, bevor ich dies verwenden kann.

Aktualisierte Version des obigen Hacks für 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)

Danke für das Update und sorry, dass ich das nicht in 3.0 bekommen habe. Ich werde versuchen, dies für die nächste Veröffentlichung zu erreichen!

Gibt es eine Chance, es in der nächsten Version zu sehen?

Vielen Dank

@Wierrat
Ja möglicherweise.

Kann uns bitte vorher mit einem Test für https://github.com/pytest-dev/pytest-django/pull/353 weiterhelfen?

Hallo Leute,
wie ist der neueste Stand dazu? Insbesondere verwende ich pytest als meinen Testläufer mit der StaticLiveServerTestCase-Klasse von Django. Ich habe das Klassenattribut serialized_rollback = True aber das scheint nur wirksam zu sein, wenn der erste Test aus der Sequenz ausgeführt wird.

Wurde gerade von diesem erwischt. Wenn man sich umsieht, scheint es einige PR zu diesem Thema gegeben zu haben, aber keine von ihnen scheint zusammengeführt worden zu sein.
Gibt es einen besonderen Grund, warum dieses Thema offen bleibt?

etwas PR

Wahrscheinlich/nur https://github.com/pytest-dev/pytest-django/issues/329 , nein?

Das letzte Mal habe ich nach Hilfe bei einem Test gefragt, und es gibt mehrere Konflikte (vielleicht nur wegen Schwarz).

Ich schlage trotzdem jedem Betroffenen vor, dabei zu helfen - ich selbst benutze es nicht.

Danke für die schnelle Antwort @blueyed.
Mein Wissen über pytest / pytest-django ist minimal, aber ich werde das irgendwann auf meine Liste setzen! :D

Aktualisierte Version des obigen Hacks für 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')

Ich musste mich auf das ursprüngliche transactional_db Fixture verlassen, anstatt es aufzurufen, da pytest es nicht mehr erlaubt, Fixture-Funktionen direkt aufzurufen. Ich erhalte dann die Fixture-Def des ursprünglichen Fixtures und stelle ihm einen Finalizer voran. Während das Einfügen bei Index 1 noch funktionierte, füge ich vor allen Finalizern ein und verwende django_db_blocker in der Wiederherstellungsfunktion, da dies etwas weniger anfällig erscheint.

Edit: unnötigen Versuch endlich entfernt.

@lukaszb

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

Ich verwende diese Version von pytest-django (https://github.com/pytest-dev/pytest-django/pull/721/files),
Aber trotzdem Fehler.

image

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

ryankask picture ryankask  ·  5Kommentare

ojake picture ojake  ·  6Kommentare

aljosa picture aljosa  ·  8Kommentare

tolomea picture tolomea  ·  4Kommentare

mpasternak picture mpasternak  ·  5Kommentare