Pytest-django: Suporta limpeza entre testes com múltiplos bancos de dados

Criado em 20 mar. 2014  ·  20Comentários  ·  Fonte: pytest-dev/pytest-django

pytest-django não limpa entre os testes ao usar Django com múltiplos bancos de dados. O problema está relacionado a esta questão StackOverflow: http://stackoverflow.com/questions/10121485/django-testcase-not-using-transactions-on-secondary-database

O fixture db usa o TestCase do Django em segundo plano.

Aqui está o código do fixture db :

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

Aqui está como eu corrigi-lo como uma solução alternativa em meu 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, esse sinalizador multi_db existe e o padrão é False por um motivo. Não tenho certeza da maneira certa de incorporar o suporte a multi_db em pytest_django, e não tenho certeza da maneira certa de testar tal mudança.

Se você tiver uma sugestão, posso trabalhar em uma solicitação pull.

Comentários muito úteis

Outra opção para habilitar o multi_db para quem procura uma solução temporária. Coloque isso em conftest.py

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

Todos 20 comentários

Você poderia dar um exemplo de teste usando sua versão corrigida que usa dois bancos de dados? Eu acho que ajudaria a entender como vários bancos de dados são usados ​​e o que você precisa de controle sobre a configuração de multi_db como true.

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

A maioria / todos os meus modelos estão associados às "operações" do segundo banco de dados. Eu acredito que o TestCase do Django irá liberar transações corretamente no banco de dados padrão, mas não em outros, a menos que multi_db=True .

Aqui estão alguns exemplos de testes:

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

Aqui está a saída com meu fixture db corrigido, descrito acima:

$ 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 =====================================

Aqui está a saída com o próprio db fixture de 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 ===============================

Então, olhando rapidamente, parece que um acessório multi_db e pytest.mark.django_db(multi=True) podem funcionar como uma API? Alguém mais tem ideias melhores?

Isso parece razoável. Certamente não tenho outras ideias, melhores ou não.

Olá. Existe alguma discussão sobre o suporte de multi_db além deste problema? Os documentos dizem que é preciso entrar em contato, mas qual é o status atual disso?

@slafs Não houve muitas discussões / solicitações para suporte a vários bancos de dados além disso.

Seria muito simples passar multi_db=True , mas também precisamos de algum tipo de teste para isso, portanto, também precisamos de um banco de dados secundário configurado. Se alguém estiver disposto a trabalhar nisso, seria ótimo :)

Eu tenho usado a solução alternativa que @ftobia menciona no post de abertura, mas depois de atualizar para o Django 1.8.4 do 1.7 ele parou de funcionar.

Houve uma mudança no Django para fazer o manuseio do código setup / tearDown olhar para um atributo de classe multi_db invés de um atributo de instância.

Alterar a solução alternativa de @ftobia para o seguinte funcionou para mim:

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:

Isso está nos impedindo de mudar do executor de teste do Django para o pytest.

Por que não usar django_case.multi_db = True aqui https://github.com/pytest-dev/pytest-django/blob/master/pytest_django/fixtures.py#L107 por padrão?

+1

+1

Acho que ter um marcador no marcador django_db como faria com uma boa API:

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

A implementação deve ser relativamente semelhante a reset_sequences (PR # 308) ou serialized_rollback (PR # 353).

A parte "complicada" seria melhorar o conjunto de testes internos do pytest-django para conter vários bancos de dados.

Se alguém quiser trabalhar nisso, com certeza irei revisá-lo e ajudar na fusão. Sinta-se à vontade para começar a trabalhar em uma RP se for algo do seu interesse!

Deixou alguns comentários em https://github.com/pytest-dev/pytest-django/pull/397#issuecomment -261987751, e criou um PR, que permitiria alterar isso de forma genérica: https: // github. com / pytest-dev / pytest-django / pull / 431.

Outra opção para habilitar o multi_db para quem procura uma solução temporária. Coloque isso em conftest.py

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

Para qualquer pessoa em busca de soluções alternativas, observe que multi_db foi descontinuado no Django 2.2. A substituição que funcionou para mim é:

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

@jcushman , você se importa em compartilhar o dispositivo completo ou o snippet de código que usou?

Por alguma razão, pytest_sessionstart() em nossa base conftest.py não estava fazendo nada. Pegou uma página de # 342 junto com pytest padrão monkeypatch stuff e pousou no seguinte para nosso uso de banco de dados múltiplo:

# 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: mudamos para um fixture de sessão._

A fim de não perder os superpoderes do pytest mudando para os casos de teste unittest do Django, copiei e apliquei o patch interno do pytest-django a este hack feio (ainda funcionando!):

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

Com ele, pode-se usar algo como:

class TestCase(object):

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

Deve ser usado em vez de db fixture ou pytest.mark.django_db mark.

@jcushman , você se importa em compartilhar o dispositivo completo ou o snippet de código que usou?

Adicionando resposta no caso de alguém topar com isso.

Você precisa adicionar o código abaixo em conftest.py . Este acessório é selecionado automaticamente, então não há necessidade de adicioná-lo a um teste.

`` `py
def pytest_sessionstart (sessão):
de django.test import TransactionTestCase

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

`` ``

Então eu acredito que esse problema se resume ao suporte multi-db adequado. Encerrarei este problema como uma duplicata daquele.

Esta página foi útil?
0 / 5 - 0 avaliações