Pytest-django: Testes usando fixture live_server removendo dados de migrações de dados

Criado em 14 abr. 2016  ·  21Comentários  ·  Fonte: pytest-dev/pytest-django

Eu criei um caso de teste simples para reproduzir este comportamento https://github.com/ekiro/case_pytest/blob/master/app/tests.py que falha após o segundo teste usando fixture live_server.
Os objetos MyModel são criados na migração, usando RunPython. Parece que depois de qualquer teste com live_server, todas as linhas do banco de dados são truncadas. Ambos, postgresql e sqlite3 foram testados.

EDITAR:
Testes

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

Comentários muito úteis

Eu também achei isso e demorei muito para rastreá-lo, embora eu esteja razoavelmente familiarizado com a estrutura de teste e as migrações do Django.

Esta é realmente uma compensação de desempenho surpreendente de TestCase do Django. Está documentado aqui:
https://docs.djangoproject.com/en/1.9/topics/testing/overview/#rollback -emulation

Em um conjunto de testes regular do Django, a solução alternativa consiste em definir um atributo de classe serialized_rollback = True no caso de teste.

Não sei como obter o mesmo efeito com as classes de teste geradas dinamicamente do pytest-django.

Todos 21 comentários

Isso é por causa da fixação transactional_db sendo usada automaticamente por live_server (não há necessidade de marcá-la com @pytest.mark.django_db ). Existem várias questões / discussões a esse respeito, mas da última vez que olhei mais de perto, não havia uma solução fácil para esse problema que vem com o uso de migrações de dados.
Uma solução alternativa é usar acessórios pytest para agrupar suas migrações de dados / acessórios de dados.
(aliás: é melhor colocar os testes embutidos no problema, em vez de em um recurso externo que pode mudar com o tempo.)

Eu também achei isso e demorei muito para rastreá-lo, embora eu esteja razoavelmente familiarizado com a estrutura de teste e as migrações do Django.

Esta é realmente uma compensação de desempenho surpreendente de TestCase do Django. Está documentado aqui:
https://docs.djangoproject.com/en/1.9/topics/testing/overview/#rollback -emulation

Em um conjunto de testes regular do Django, a solução alternativa consiste em definir um atributo de classe serialized_rollback = True no caso de teste.

Não sei como obter o mesmo efeito com as classes de teste geradas dinamicamente do pytest-django.

A alteração a seguir "resolve" o problema à custa de selecionar incondicionalmente o comportamento 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)

A técnica a seguir funciona, mas não posso recomendá-la por razões bastante óbvias ...

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)

Uma opção para habilitar serialized_rollback condicionalmente seria uma boa solução?

Ou seja, algo como

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

Eu acho que isso seria bom.

No meu caso de uso:

  • o fixture transactional_db é acionado por live_server , mas eu não me importaria de adicionar um fixture django_db para especificar reversão serializada
  • não relacionado a pytest-django - case.serialized_rollback = True ainda falha porque o Django tenta desserializar os objetos em uma ordem que não respeita as restrições FK, acho que é um bug no Django

Ok perfeito :)

Não tenho tempo para trabalhar em uma correção agora, mas adicionar essa opção ao marcador django_db e definir case.serialized_rollback = True (como você fez acima) deve ser relativamente simples.

Ingresso para o bug do Django que mencionei acima: https://code.djangoproject.com/ticket/26552

@pelme - esta solicitação pull implementa a abordagem "relativamente direta" que você está imaginando?

Muito obrigado pelo PR. A implementação parece correta e o que eu imaginava. Eu não pensei em ter um aparelho serialized_rollback, mas é realmente útil ser capaz de forçar esse comportamento de outros aparelhos.

Também precisaríamos de um teste para isso, mas a implementação parece correta!

Vou tentar encontrar tempo para polir o patch (ainda nem o executei). Também preciso consertar o bug mencionado no Django antes de usar isso.

Versão atualizada do hack acima 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)

Obrigado pela atualização e desculpe por não ter conseguido entrar no 3.0. Vou tentar fazer isso no próximo lançamento!

Existe alguma chance de vê-lo no próximo lançamento?

Obrigado

@Wierrat
Sim provavelmente.

Pode nos ajudar com um teste para https://github.com/pytest-dev/pytest-django/pull/353 antes disso, por favor?

Oi pessoal,
qual é o status mais recente disso? Em particular, estou usando o pytest como meu executor de teste com a classe StaticLiveServerTestCase do Django. Eu defini o atributo de classe serialized_rollback = True mas ele parece estar em vigor apenas ao executar o primeiro teste da sequência.

Só fui pego por este. Olhando em volta, parece que houve algum RP sobre esse problema, mas nenhum deles parece ter sido mesclado.
Existe algum motivo específico para esse problema permanecer aberto?

algum PR

Provável / apenas https://github.com/pytest-dev/pytest-django/issues/329 , não?

A última vez que pedi ajuda com um teste, há vários conflitos (talvez apenas devido ao preto).

Ainda sugiro que qualquer pessoa afetada ajude com isso - não estou usando sozinho.

Obrigado pela resposta rápida @blueyed.
Meu conhecimento de pytest / pytest-django é mínimo, mas vou colocá-lo na minha lista de algum dia / talvez! : D

Versão atualizada do hack acima 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')

Tive que depender do fixture transactional_db vez de chamá-lo porque o pytest não permite mais chamar funções de fixture diretamente. Eu então pego a definição de fixação da fixação original e acrescento um finalizador a ela. Embora a inserção no índice 1 ainda funcione, eu insiro antes de todos os finalizadores e uso django_db_blocker na função de restauração, pois parece um pouco menos frágil.

Editar: removeu a tentativa desnecessária finalmente.

@lukaszb

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

Eu uso esta versão do pytest-django (https://github.com/pytest-dev/pytest-django/pull/721/files),
Mas ainda erro.

image

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