Pytest-django: Тесты с использованием фикстуры live_server для удаления данных из миграции данных

Созданный на 14 апр. 2016  ·  21Комментарии  ·  Источник: pytest-dev/pytest-django

Я создал простой тестовый пример для воспроизведения этого поведения https://github.com/ekiro/case_pytest/blob/master/app/tests.py, который не работает после второго теста с использованием фикстуры live_server.
Объекты MyModel создаются при миграции с использованием RunPython. Похоже, что после любого теста с live_server каждая строка из базы данных усекается. Оба, postgresql и sqlite3 были протестированы.

РЕДАКТИРОВАТЬ:
Тесты

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

Самый полезный комментарий

Я просто столкнулся с этим, и мне потребовалось много времени, чтобы его отследить, хотя я достаточно знаком с тестовой структурой и миграциями Django.

На самом деле это удивительный компромисс в производительности TestCase Django. Это задокументировано здесь:
https://docs.djangoproject.com/en/1.9/topics/testing/overview/#rollback -emulation

В обычном наборе тестов Django обходной путь заключается в установке атрибута serialized_rollback = True class в тестовом примере.

Я не знаю, как добиться того же эффекта с динамически генерируемыми тестовыми классами pytest-django.

Все 21 Комментарий

Это потому, что прибор transactional_db автоматически используется live_server (нет необходимости отмечать его с помощью @pytest.mark.django_db ). В этом отношении есть несколько вопросов / обсуждений, но в последний раз, когда я присматривался к этому, не было простого решения этой проблемы, связанной с использованием миграции данных.
Обходной путь - использовать фикстуры pytest, чтобы обернуть ваши миграции данных / фикстуры данных.
(Кстати: лучше поместить тесты прямо в задачу, а не во внешний ресурс, который со временем может измениться.)

Я просто столкнулся с этим, и мне потребовалось много времени, чтобы его отследить, хотя я достаточно знаком с тестовой структурой и миграциями Django.

На самом деле это удивительный компромисс в производительности TestCase Django. Это задокументировано здесь:
https://docs.djangoproject.com/en/1.9/topics/testing/overview/#rollback -emulation

В обычном наборе тестов Django обходной путь заключается в установке атрибута serialized_rollback = True class в тестовом примере.

Я не знаю, как добиться того же эффекта с динамически генерируемыми тестовыми классами pytest-django.

Следующее изменение «решает» проблему за счет безоговорочного выбора наименее эффективного поведения.

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

Следующая техника работает, но я не могу ее рекомендовать по довольно очевидным причинам ...

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)

Будет ли вариант условного включения serialized_rollback хорошим решением?

Т.е. что-то вроде

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

Думаю, это было бы хорошо.

В моем случае использования:

  • приспособление transactional_db запускается live_server , но я бы не возражал добавить приспособление django_db для указания сериализованного отката
  • не связано с pytest-django - case.serialized_rollback = True прежнему терпит неудачу, потому что Django пытается десериализовать объекты в порядке, который не соблюдает ограничения FK, я думаю, что это ошибка в Django

Ок, прекрасно :)

У меня нет времени работать над исправлением прямо сейчас, но добавление такой опции к маркеру django_db и установка case.serialized_rollback = True (как вы это делали выше) должны быть относительно простыми.

Билет по ошибке Django, о которой я упоминал выше: https://code.djangoproject.com/ticket/26552

@pelme -

Большое спасибо за пиар. Реализация выглядит правильной и такой, как я себе представлял. Я не думал о фикстуре serialized_rollback, но действительно полезно иметь возможность принудительно использовать это поведение из других фикстур.

Для этого нам тоже понадобится тест, но реализация выглядит правильно!

Я постараюсь найти время, чтобы отполировать патч (я его еще даже не запускал). Мне также нужно исправить упомянутую ошибку в Django, прежде чем я смогу это использовать.

Обновленная версия взлома выше для 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)

Спасибо за обновление и извините, что не смог добавить его в 3.0. Я постараюсь добраться до этого в следующем выпуске!

Есть ли шанс увидеть это в следующем выпуске?

Спасибо

@Wierrat
Да, возможно.

Можете помочь нам с тестом для https://github.com/pytest-dev/pytest-django/pull/353 перед этим, тогда, пожалуйста?

Привет народ,
каков последний статус этого? В частности, я использую pytest в качестве средства запуска тестов с классом StaticLiveServerTestCase из Django. Я установил атрибут класса serialized_rollback = True но, похоже, он действует только при выполнении первого теста из последовательности.

Просто попался на этот раз. Оглядываясь вокруг, кажется, что по этому поводу был какой-то пиар, но ни один из них, похоже, не был объединен.
Есть ли какая-то конкретная причина, по которой этот вопрос остается открытым?

немного пиара

Скорее всего / только https://github.com/pytest-dev/pytest-django/issues/329 , нет?

В последний раз просил помочь с тестом, и у него несколько конфликтов (может, только из-за черного).

Я все еще предлагаю всем пострадавшим помочь с этим - я сам этим не пользуюсь.

Спасибо за быстрый ответ @blueyed.
Мои познания в pytest / pytest-django минимальны, но я внесу это в свой список на сегодня / может быть! : D

Обновленная версия вышеупомянутого хака для 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')

Мне пришлось полагаться на исходную фикстуру transactional_db вместо ее вызова, поскольку pytest больше не позволяет напрямую вызывать функции фикстуры. Затем я получаю определение исходной фиксации и добавляю к ней финализатор. Хотя вставка в индекс 1 все еще работает, я вставляю перед всеми финализаторами и использую django_db_blocker в функции восстановления, поскольку это кажется немного менее хрупким.

Изменить: наконец удалил ненужную попытку.

@lukaszb

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

Я использую эту версию pytest-django (https://github.com/pytest-dev/pytest-django/pull/721/files),
Но все же ошибка.

image

Была ли эта страница полезной?
0 / 5 - 0 рейтинги

Смежные вопросы

tolomea picture tolomea  ·  4Комментарии

dan-passaro picture dan-passaro  ·  4Комментарии

mpasternak picture mpasternak  ·  5Комментарии

blueyed picture blueyed  ·  7Комментарии

rlskoeser picture rlskoeser  ·  7Комментарии