Pytest-django: 使用 live_server 夹具从数据迁移中删除数据的测试

创建于 2016-04-14  ·  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 的测试框架和迁移相当熟悉,但我花了很长时间才找到它。

这实际上是 Django 的TestCase令人惊讶的性能折衷。 它记录在这里:
https://docs.djangoproject.com/en/1.9/topics/testing/overview/#rollback -emulation

在常规 Django 测试套件中,解决方法包括在测试用例上设置serialized_rollback = True类属性。

我不知道如何用pytest-django的动态生成的测试类来达到同样的效果。

所有21条评论

这是因为live_server自动使用了transactional_db固定装置(不需要用@pytest.mark.django_db标记它)。 在这方面有几个问题/讨论,但上次我仔细研究这个问题时,使用数据迁移并没有简单的解决方案来解决这个问题。
一种解决方法是使用 pytest 固定装置来包装您的数据迁移/数据固定装置。
(顺便说一句:最好将测试放入问题中,而不是放入可能随时间变化的外部资源中。)

我也遇到了这个问题,虽然我对 Django 的测试框架和迁移相当熟悉,但我花了很长时间才找到它。

这实际上是 Django 的TestCase令人惊讶的性能折衷。 它记录在这里:
https://docs.djangoproject.com/en/1.9/topics/testing/overview/#rollback -emulation

在常规 Django 测试套件中,解决方法包括在测试用例上设置serialized_rollback = True类属性。

我不知道如何用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 bug 的票证: https :

@pelme——这个拉取请求是否实现了你设想的“相对简单”的方法?

非常感谢公关。 实现看起来是正确的,也是我想象的。 我没有想到有一个 serialized_rollback 固定装置,但能够从其他固定装置强制这种行为确实很有用。

我们也需要对此进行测试,但实现看起来是正确的!

我会尽量找时间完善补丁(我还没有运行它)。 在使用它之前,我还需要修复 Django 中的 aformentionned 错误。

上面针对 pytest-django ≥ 3.0 的 hack 的更新版本:

@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。 我将在下一个版本中尝试解决这个问题!

下个版本还有机会看到吗?

谢谢

@维拉特
很可能是。

可以帮助我们在此之前对https://github.com/pytest-dev/pytest-django/pull/353进行测试吗?

嗨伙计,
这是什么最新状态? 特别是,我使用 pytest 作为我的测试运行程序和来自 Django 的 StaticLiveServerTestCase 类。 我已经设置了类属性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 的上述 hack 的更新版本:

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 不再允许直接调用固定装置函数。 然后我得到原始夹具的夹具 def 并在它前面添加一个终结器。 虽然在索引 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 等级