diff --git a/docs/changelog.rst b/docs/changelog.rst index 5e4a22306..e6b67c5f2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,12 @@ Bug fixes Thanks to Will Harris for `the bug report `_. +Features +^^^^^^^^ +* Add support for serialized rollback in transactional tests. + Thanks to Piotr Karkut for `the bug report + `_. + Features ^^^^^^^^ * Added a new option `--migrations` to negate a default usage of diff --git a/docs/helpers.rst b/docs/helpers.rst index 7c60f9005..1860ee1fd 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -16,7 +16,7 @@ on what marks are and for notes on using_ them. ``pytest.mark.django_db`` - request database access ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. py:function:: pytest.mark.django_db([transaction=False]) +.. py:function:: pytest.mark.django_db([transaction=False, serialized_rollback=False]) This is used to mark a test function as requiring the database. It will ensure the database is setup correctly for the test. Each test @@ -38,6 +38,14 @@ on what marks are and for notes on using_ them. uses. When ``transaction=True``, the behavior will be the same as `django.test.TransactionTestCase`_ + :type serialized_rollback: bool + :param serialized_rollback: + The ``serialized_rollback`` argument enables `rollback emulation`_. + After a `django.test.TransactionTestCase`_ runs, the database is + flushed, destroying data created in data migrations. This is the + default behavior of Django. Setting ``serialized_rollback=True`` + tells Django to restore that data. + .. note:: If you want access to the Django database *inside a fixture* @@ -54,6 +62,7 @@ on what marks are and for notes on using_ them. Test classes that subclass Python's ``unittest.TestCase`` need to have the marker applied in order to access the database. +.. _rollback emulation: https://docs.djangoproject.com/en/stable/topics/testing/overview/#rollback-emulation .. _django.test.TestCase: https://docs.djangoproject.com/en/dev/topics/testing/overview/#testcase .. _django.test.TransactionTestCase: https://docs.djangoproject.com/en/dev/topics/testing/overview/#transactiontestcase @@ -191,6 +200,16 @@ transaction support. This is only required for fixtures which need database access themselves. A test function would normally use the :py:func:`~pytest.mark.django_db` mark to signal it needs the database. +``serialized_rollback`` +~~~~~~~~~~~~~~~~~~~~~~~ + +When the ``transactional_db`` fixture is enabled, this fixture can be +added to trigger `rollback emulation`_ and thus restores data created +in data migrations after each transaction test. This is only required +for fixtures which need to enforce this behavior. A test function +would use :py:func:`~pytest.mark.django_db(serialized_rollback=True)` +to request this behavior. + ``live_server`` ~~~~~~~~~~~~~~~ @@ -200,6 +219,12 @@ or by requesting it's string value: ``unicode(live_server)``. You can also directly concatenate a string to form a URL: ``live_server + '/foo``. +Since the live server and the tests run in different threads, they +cannot share a database transaction. For this reason, ``live_server`` +depends on the ``transactional_db`` fixture. If tests depend on data +created in data migrations, you should add the ``serialized_rollback`` +fixture. + ``settings`` ~~~~~~~~~~~~ diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 523b0d052..ac0d1063c 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -63,7 +63,8 @@ def teardown_database(): request.addfinalizer(teardown_database) -def _django_db_fixture_helper(transactional, request, _django_cursor_wrapper): +def _django_db_fixture_helper(transactional, serialized_rollback, + request, _django_cursor_wrapper): if is_django_unittest(request): return @@ -83,6 +84,7 @@ def _django_db_fixture_helper(transactional, request, _django_cursor_wrapper): if django_case: case = django_case(methodName='__init__') + case.serialized_rollback = serialized_rollback case._pre_setup() request.addfinalizer(case._post_teardown) @@ -115,7 +117,9 @@ def db(request, _django_db_setup, _django_cursor_wrapper): or 'live_server' in request.funcargnames: request.getfuncargvalue('transactional_db') else: - _django_db_fixture_helper(False, request, _django_cursor_wrapper) + _django_db_fixture_helper( + transactional=False, serialized_rollback=False, + request=request, _django_cursor_wrapper=_django_cursor_wrapper) @pytest.fixture(scope='function') @@ -130,7 +134,23 @@ def transactional_db(request, _django_db_setup, _django_cursor_wrapper): database setup will behave as only ``transactional_db`` was requested. """ - _django_db_fixture_helper(True, request, _django_cursor_wrapper) + # TODO -- is request.getfuncargvalue('serialized_rollback') enough + # to add 'serialized_rollback' to request.funcargnames? + serialized_rollback = 'serialized_rollback' in request.funcargnames + _django_db_fixture_helper(transactional=True, + serialized_rollback=serialized_rollback, + request=request, + _django_cursor_wrapper=_django_cursor_wrapper) + + +@pytest.fixture(scope='function') +def serialized_rollback(request): + """Enable serialized rollback after transaction test cases + + This fixture only has an effect when the ``transactional_db`` + fixture is active, which happen as a side-effect of requesting + ``live_server``. + """ @pytest.fixture() diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 85e8e8594..40892a741 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -361,6 +361,8 @@ def _django_db_marker(request): request.getfuncargvalue('transactional_db') else: request.getfuncargvalue('db') + if marker.serialized_rollback: + request.getfuncargvalue('serialized_rollback') @pytest.fixture(autouse=True, scope='class') @@ -559,8 +561,9 @@ def validate_django_db(marker): It checks the signature and creates the `transaction` attribute on the marker which will have the correct value. """ - def apifun(transaction=False): + def apifun(transaction=False, serialized_rollback=False): marker.transaction = transaction + marker.serialized_rollback = serialized_rollback apifun(*marker.args, **marker.kwargs)