Pytest-django: Soporte de limpieza entre pruebas con múltiples bases de datos

Creado en 20 mar. 2014  ·  20Comentarios  ·  Fuente: pytest-dev/pytest-django

pytest-django no se limpia entre pruebas cuando se usa Django con múltiples bases de datos. El problema está relacionado con esta pregunta de StackOverflow: http://stackoverflow.com/questions/10121485/django-testcase-not-using-transactions-on-secondary-database

El accesorio db usa TestCase de Django debajo de las cubiertas.

Aquí está el código del accesorio db :

case = TestCase(methodName='__init__')
case._pre_setup()
request.addfinalizer(case._post_teardown)
request.addfinalizer(_django_cursor_wrapper.disable)

Así es como lo parcheé como una solución en mi código:

case = TestCase(methodName='__init__')
case.multi_db = True
case._pre_setup()
request.addfinalizer(case._post_teardown)
request.addfinalizer(_django_cursor_wrapper.disable)

Obviamente, esa bandera multi_db existe y por defecto es False por una razón. No estoy seguro de la forma correcta de incorporar el soporte multi_db en pytest_django, y no estoy seguro de la forma correcta de probar dicho cambio.

Si tiene una sugerencia, puedo trabajar en una solicitud de extracción.

Comentario más útil

Otra opción para habilitar multi_db para aquellos que buscan una solución temporal. Pon esto en conftest.py

def pytest_sessionstart(session):
    from django.test import TransactionTestCase
    TransactionTestCase.multi_db = True

Todos 20 comentarios

¿Podría dar una prueba de ejemplo usando su versión parcheada que usa dos bases de datos? Creo que eso ayudaría a comprender cómo se usan varias bases de datos y qué necesita control sobre otras, luego de configurar multi_db en verdadero.

DATABASES = {
    'default': {
        'ENGINE'   : 'django.db.backends.sqlite3',
        'NAME'     : 'default.db',
    },
    'operations': {
        'ENGINE'   : 'django.db.backends.sqlite3',
        'NAME'     : 'operations.db',
    },
}

La mayoría / todos mis modelos están asociados con las segundas "operaciones" de la base de datos. Creo que TestCase de Django vaciará las transacciones correctamente en la base de datos predeterminada, pero no en otras a menos que multi_db=True .

Aquí hay algunas pruebas de muestra:

from collection.models import Foo

def test_one(db):
    Foo.objects.create()
    assert Foo.objects.count() == 1

def test_two(db):
    assert Foo.objects.count() == 0

Aquí está la salida con mi accesorio db parcheado, descrito arriba:

$ py.test multi_db_test/ -vv
======================================= test session starts =======================================
platform linux2 -- Python 2.7.2 -- py-1.4.20 -- pytest-2.5.2 -- bin/python
plugins: django
collected 2 items

multi_db_test/test_foo.py:3: test_one PASSED
multi_db_test/test_foo.py:7: test_two PASSED

==================================== 2 passed in 5.75 seconds =====================================

Aquí está la salida con el propio accesorio db pytest_django:

$ py.test multi_db_test/ -vv
======================================= test session starts =======================================
platform linux2 -- Python 2.7.2 -- py-1.4.20 -- pytest-2.5.2 -- bin/python
plugins: django
collected 2 items

multi_db_test/test_foo.py:3: test_one PASSED
multi_db_test/test_foo.py:7: test_two FAILED

============================================ FAILURES =============================================
____________________________________________ test_two _____________________________________________

db = None

    def test_two(db):
>       assert Foo.objects.count() == 0
E       assert 1 == 0
E        +  where 1 = <bound method Manager.count of <django.db.models.manager.Manager object at 0x3cd0890>>()
E        +    where <bound method Manager.count of <django.db.models.manager.Manager object at 0x3cd0890>> = <django.db.models.manager.Manager object at 0x3cd0890>.count
E        +      where <django.db.models.manager.Manager object at 0x3cd0890> = Foo.objects

multi_db_test/test_foo.py:8: AssertionError
=============================== 1 failed, 1 passed in 5.85 seconds ===============================

Entonces, de un vistazo rápido, parece que un multi_db accesorio y pytest.mark.django_db(multi=True) podrían funcionar como una API. ¿Alguien más tiene mejores ideas?

Eso suena razonable. Ciertamente no tengo otras ideas, mejores o no.

Hola. ¿Hay alguna discusión sobre el soporte de multi_db además de este tema? Los documentos dicen que uno debe ponerse en contacto, pero ¿cuál es el estado actual de esto?

@slafs No ha habido muchas discusiones / solicitudes para soporte de múltiples bases de datos aparte de esto.

Sería bastante simple pasar multi_db=True , pero también necesitamos algún tipo de pruebas para esto, por lo que también necesitamos una base de datos secundaria configurada. Si alguien está dispuesto a trabajar en esto, sería genial :)

He estado usando las menciones de @ftobia en la publicación de apertura, pero después de actualizar a Django 1.8.4 desde 1.7 dejó de funcionar.

Hubo un cambio en Django para hacer que la configuración de manejo de código / tearDown buscara un atributo de clase multi_db lugar de un atributo de instancia.

Cambiar la solución alternativa de

case = TestCase(methodName='__init__')
case.__class__.multi_db = True
case._pre_setup()
request.addfinalizer(case._post_teardown)
request.addfinalizer(_django_cursor_wrapper.disable)

+1: +1:

Esto nos impide cambiar del ejecutor de pruebas de Django a pytest.

¿Por qué no usar django_case.multi_db = True aquí https://github.com/pytest-dev/pytest-django/blob/master/pytest_django/fixtures.py#L107 de forma predeterminada?

+1

+1

Creo que tener un marcador en el marcador django_db como lo haría una buena API:

@pytest.mark.django_db(transactional=True, multi_db=True)
def test_a():
    pass

La implementación debería ser relativamente similar a reset_sequences (PR # 308) o serialized_rollback (PR # 353).

La parte "complicada" sería mejorar el conjunto de pruebas internas de pytest-django para que contenga múltiples bases de datos.

Si alguien quiere trabajar en esto, seguramente lo revisaré y ayudaré a fusionarlo. ¡Siéntete libre de comenzar a trabajar en un PR si esto es algo que sería interesante para ti!

Dejó algunos comentarios en https://github.com/pytest-dev/pytest-django/pull/397#issuecomment -261987751, y creó un PR, que permitiría cambiar esto de manera genérica: https: // github. com / pytest-dev / pytest-django / pull / 431.

Otra opción para habilitar multi_db para aquellos que buscan una solución temporal. Pon esto en conftest.py

def pytest_sessionstart(session):
    from django.test import TransactionTestCase
    TransactionTestCase.multi_db = True

Para cualquiera que busque soluciones alternativas, tenga en cuenta que multi_db quedó obsoleto en Django 2.2. El reemplazo que funcionó para mí es:

TransactionTestCase.databases = set(settings.DATABASES.keys())

@jcushman, ¿te importaría compartir el accesorio completo o el fragmento de código que usaste?

Por alguna razón, pytest_sessionstart() en nuestra base conftest.py no estaba haciendo nada. Tomó una página de # 342 junto con pytest estándar monkeypatch cosas y aterrizó en lo siguiente para nuestro uso de múltiples bases de datos:

# OPTION 1: Function fixture; must be included with tests
@pytest.fixture
def django_db_multiple(monkeypatch, request, settings):
    """
    Ensure all test functions using Django test cases have multiple database
    support. This is mostly/only so that Django will wrap ALL database use with
    atomic blocks like it does for DEFAULT_DB_ALIAS.

    https://github.com/django/django/blob/master/django/test/testcases.py#L903
    https://github.com/pytest-dev/pytest-django/issues/76
    """
    from django.test import TestCase
    from django.test import TransactionTestCase

    db_keys = set(settings.DATABASES.keys())

    monkeypatch.setattr(TestCase, 'databases', db_keys)
    monkeypatch.setattr(TransactionTestCase, 'databases', db_keys)

@pytest.mark.django_db
def test_some_test(django_db_multiple):
    pass

# OPTION 2: Session fixture
@pytest.fixture(autouse=True, scope='session')
def django_db_multiple():
    """
    Ensure all test functions using Django test cases have multiple database
    support. This is mostly/only so that Django will wrap ALL database use with
    atomic blocks like it does for DEFAULT_DB_ALIAS.

    https://github.com/django/django/blob/master/django/test/testcases.py#L903
    https://github.com/pytest-dev/pytest-django/issues/76
    https://github.com/pytest-dev/pytest/issues/1872
    """
    from _pytest.monkeypatch import MonkeyPatch
    from django.test import TestCase
    from django.test import TransactionTestCase
    from django.conf import settings

    db_keys = set(settings.DATABASES.keys())

    monkeypatch = MonkeyPatch()
    monkeypatch.setattr(TestCase, 'databases', db_keys)
    monkeypatch.setattr(TransactionTestCase, 'databases', db_keys)

    yield monkeypatch

    monkeypatch.undo()

_Editar: nos trasladamos a un fixture de sesión_

Para no perder los superpoderes de Pytest al cambiar a los casos de prueba de Unittest Django, copié y parcheé los componentes internos de pytest-django en este truco feo (¡pero que funciona!):

from typing import Optional, Type, TypeVar

import pytest
from django.test import TransactionTestCase
from pytest_django.django_compat import is_django_unittest

TestCase = TypeVar('TestCase', bound=TransactionTestCase)


def _django_db_fixture_helper(
    request, django_db_blocker, transactional: bool = False, reset_sequences: bool = False,
) -> Optional[Type[TestCase]]:
    if is_django_unittest(request):
        return None

    if not transactional and 'live_server' in request.fixturenames:
        # Do nothing, we get called with transactional=True, too.
        return None

    django_db_blocker.unblock()
    request.addfinalizer(django_db_blocker.restore)

    if transactional:
        from django.test import TransactionTestCase as DjangoTestCase  # noqa: WPS433

        if reset_sequences:

            class ResetSequenceTestCase(DjangoTestCase):  # noqa: WPS431
                reset_sequences = True

            DjangoTestCase = ResetSequenceTestCase  # type: ignore[misc] # noqa: N806
    else:
        from django.test import TestCase as DjangoTestCase  # type: ignore[no-redef] # noqa: WPS433

    return DjangoTestCase  # type: ignore[return-value]


@pytest.fixture()
def db_case(request, django_db_setup, django_db_blocker):
    """Require a django test database.

    This database will be setup with the default fixtures and will have
    the transaction management disabled. At the end of the test the outer
    transaction that wraps the test itself will be rolled back to undo any
    changes to the database (in case the backend supports transactions).
    This is more limited than the ``transactional_db`` resource but
    faster.

    If multiple database fixtures are requested, they take precedence
    over each other in the following order (the last one wins): ``db``,
    ``transactional_db``, ``django_db_reset_sequences``.
    """
    if 'django_db_reset_sequences' in request.fixturenames:
        request.getfixturevalue('django_db_reset_sequences')
    if (
        'transactional_db' in request.fixturenames
        or 'live_server' in request.fixturenames
    ):
        request.getfixturevalue('transactional_db')
    else:
        django_case: Optional[Type[TransactionTestCase]] = _django_db_fixture_helper(
            request, django_db_blocker, transactional=False,
        )

        def factory(dbs=None):  # noqa: WPS430
            if django_case is None:
                return
            CaseType: Type[TransactionTestCase] = django_case  # noqa: N806
            if dbs is not None:
                class DatabasesSetTestCase(  # noqa: WPS431
                    CaseType,  # type: ignore[valid-type, misc]
                ):
                    databases = dbs
                CaseType = DatabasesSetTestCase  # noqa: N806
            test_case: TransactionTestCase = CaseType(methodName='__init__')
            test_case._pre_setup()  # type: ignore[attr-defined] # noqa: WPS437
            request.addfinalizer(
                test_case._post_teardown,  # type: ignore[attr-defined] # noqa: WPS437
            )

        return factory

Con él se puede usar algo como:

class TestCase(object):

    def test_ok(
        self,
        db_case,
        ...
    ):
        db_case(dbs=('non_default_db_alias',))

Esto debe usarse en lugar de db fixture o pytest.mark.django_db mark.

@jcushman, ¿te importaría compartir el accesorio completo o el fragmento de código que usaste?

Agregar respuesta en caso de que alguien más se encuentre con esto.

Debe agregar el código a continuación en conftest.py . Este dispositivo se selecciona automáticamente, por lo que no es necesario agregarlo a una prueba.

`` py
def pytest_sessionstart (sesión):
desde django.test importar TransactionTestCase

TransactionTestCase.databases = set(settings.DATABASES.keys())

`` ``

Así que creo que este problema se reduce al soporte adecuado de múltiples bases de datos. Cerraré este problema como duplicado.

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