Pytest-django: Pruebas utilizando el dispositivo live_server eliminando datos de las migraciones de datos

Creado en 14 abr. 2016  ·  21Comentarios  ·  Fuente: pytest-dev/pytest-django

Creé un caso de prueba simple para reproducir este comportamiento https://github.com/ekiro/case_pytest/blob/master/app/tests.py que falla después de la segunda prueba usando el accesorio live_server.
Los objetos MyModel se crean en la migración, utilizando RunPython. Parece que después de cualquier prueba con live_server, todas las filas de la base de datos se truncan. Se probaron tanto postgresql como sqlite3.

EDITAR:
Pruebas

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

Comentario más útil

Acabo de llegar a esto también y me tomó mucho tiempo rastrearlo, a pesar de que estoy razonablemente familiarizado con el marco de prueba y las migraciones de Django.

Esta es en realidad una compensación de rendimiento sorprendente de TestCase de Django. Está documentado aquí:
https://docs.djangoproject.com/en/1.9/topics/testing/overview/#rollback -emulation

En un conjunto de pruebas normal de Django, la solución consiste en establecer un atributo de clase serialized_rollback = True en el caso de prueba.

No sé cómo lograr el mismo efecto con las clases de prueba generadas dinámicamente de pytest-django.

Todos 21 comentarios

Esto se debe a que el accesorio transactional_db está siendo utilizado automáticamente por live_server (no es necesario marcarlo con @pytest.mark.django_db ). Hay varios temas / discusiones a este respecto, pero la última vez que miré más de cerca, no había una solución fácil para este problema que viene con el uso de migraciones de datos.
Una solución alternativa es usar accesorios de pytest para envolver sus migraciones de datos / accesorios de datos.
(Por cierto: es mejor poner las pruebas en línea en el problema en lugar de en un recurso externo que podría cambiar con el tiempo).

Acabo de llegar a esto también y me tomó mucho tiempo rastrearlo, a pesar de que estoy razonablemente familiarizado con el marco de prueba y las migraciones de Django.

Esta es en realidad una compensación de rendimiento sorprendente de TestCase de Django. Está documentado aquí:
https://docs.djangoproject.com/en/1.9/topics/testing/overview/#rollback -emulation

En un conjunto de pruebas normal de Django, la solución consiste en establecer un atributo de clase serialized_rollback = True en el caso de prueba.

No sé cómo lograr el mismo efecto con las clases de prueba generadas dinámicamente de pytest-django.

El siguiente cambio "resuelve" el problema a expensas de seleccionar incondicionalmente el comportamiento menos eficiente.

--- 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 siguiente técnica funciona, pero no puedo recomendarla por razones bastante obvias ...

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)

¿Sería una buena solución una opción para habilitar condicionalmente serialized_rollback ?

Es decir, algo como

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

Creo que eso estaría bien.

En mi caso de uso:

  • el accesorio transactional_db es activado por live_server , pero no me importaría agregar un accesorio django_db para especificar la reversión serializada
  • no relacionado con pytest-django - case.serialized_rollback = True todavía falla porque Django intenta objetos deserializados en un orden que no respeta las restricciones de FK, creo que eso es un error en Django

OK perfecto :)

No tengo tiempo para trabajar en una solución en este momento, pero agregar esa opción al marcador django_db debería y configurar case.serialized_rollback = True (como lo hizo anteriormente) debería ser relativamente sencillo.

Ticket para el error de Django que mencioné anteriormente: https://code.djangoproject.com/ticket/26552

@pelme : ¿esta solicitud de extracción implementa el enfoque "relativamente sencillo" que está imaginando?

Muchas gracias por las relaciones públicas. La implementación parece correcta y lo que imaginé. No pensé en tener un dispositivo serialized_rollback, pero de hecho es útil poder forzar este comportamiento desde otros dispositivos.

Necesitaríamos una prueba para esto también, ¡pero la implementación parece correcta!

Intentaré encontrar tiempo para pulir el parche (aún no lo he ejecutado). Necesito arreglar el error mencionado en Django antes de poder usar esto.

Versión actualizada del truco anterior para 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)

Gracias por la actualización y lo siento, no pude poner esto en 3.0. ¡Intentaré llegar a esto para el próximo lanzamiento!

¿Hay alguna posibilidad de verlo en la próxima versión?

Gracias

@Wierrat
Si probablemente.

¿Puede ayudarnos con una prueba para https://github.com/pytest-dev/pytest-django/pull/353 antes de eso, entonces, por favor?

Hola amigos,
¿Cuál es el estado más reciente de esto? En particular, estoy usando pytest como mi corredor de pruebas con la clase StaticLiveServerTestCase de Django. Establecí el atributo de clase serialized_rollback = True pero eso parece estar en efecto solo cuando se ejecuta la primera prueba de la secuencia.

Solo me atrapó este. Mirando a su alrededor, parece que ha habido algunas relaciones públicas sobre este tema, pero ninguno de ellos parece haberse fusionado.
¿Hay alguna razón en particular por la que este tema permanezca abierto?

algunas relaciones públicas

Probablemente / solo https://github.com/pytest-dev/pytest-django/issues/329 , ¿no?

La última vez que he pedido ayuda con una prueba, tiene varios conflictos (tal vez solo debido al negro).

Todavía sugiero a cualquier persona afectada que ayude con eso, no lo estoy usando yo mismo.

Gracias por la rápida respuesta @blueyed.
¡Mi conocimiento de pytest / pytest-django es mínimo, pero lo pondré en mi lista de somday / tal vez! :D

Versión actualizada del truco anterior para 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')

Tuve que depender del dispositivo transactional_db lugar de llamarlo, ya que pytest ya no permite llamar directamente a las funciones del dispositivo. Luego obtengo la definición de accesorio del accesorio original y le agrego un finalizador. Si bien la inserción en el índice 1 aún funcionaba, inserto antes de todos los finalizadores y uso django_db_blocker en la función de restauración, ya que parece un poco menos frágil.

Editar: eliminó el intento innecesario finalmente.

@lukaszb

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

Yo uso esta versión de pytest-django (https://github.com/pytest-dev/pytest-django/pull/721/files),
Pero sigue siendo un error.

image

¿Fue útil esta página
0 / 5 - 0 calificaciones