Skip to content

Commit d6ea40f

Browse files
committed
Add support for rollback emulation/serialized rollback
Thanks to Aymeric Augustin, Daniel Hahler and Fábio C. Barrionuevo da Luz for previous work on this feature. Fix #329. Closes #353. Closes #721. Closes #919. Closes #956.
1 parent 904a995 commit d6ea40f

File tree

6 files changed

+192
-21
lines changed

6 files changed

+192
-21
lines changed

docs/changelog.rst

+9-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,18 @@ Changelog
44
unreleased
55
----------
66

7+
Improvements
8+
^^^^^^^^^^^^
9+
10+
* Add support for :ref:`rollback emulation/serialized rollback
11+
<test-case-serialized-rollback>`. The :func:`pytest.mark.django_db` marker
12+
has a new ``serialized_rollback`` option, and a
13+
:fixture:`django_db_serialized_rollback` fixture is added.
14+
715
Bugfixes
816
^^^^^^^^
917

10-
* Fix :fixture:`live_server` when using an in-memory SQLite database on
11-
Django >= 3.0.
18+
* Fix :fixture:`live_server` when using an in-memory SQLite database.
1219

1320

1421
v4.4.0 (2021-06-06)

docs/helpers.rst

+37-8
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,27 @@ dynamically in a hook or fixture.
7373
For details see :py:attr:`django.test.TransactionTestCase.databases` and
7474
:py:attr:`django.test.TestCase.databases`.
7575

76+
:type serialized_rollback: bool
77+
:param serialized_rollback:
78+
The ``serialized_rollback`` argument enables :ref:`rollback emulation
79+
<test-case-serialized-rollback>`. After a transactional test (or any test
80+
using a database backend which doesn't support transactions) runs, the
81+
database is flushed, destroying data created in data migrations. Setting
82+
``serialized_rollback=True`` tells Django to serialize the database content
83+
during setup, and restore it during teardown.
84+
85+
Note that this will slow down that test suite by approximately 3x.
86+
7687
.. note::
7788

7889
If you want access to the Django database inside a *fixture*, this marker may
7990
or may not help even if the function requesting your fixture has this marker
80-
applied, depending on pytest's fixture execution order. To access the
81-
database in a fixture, it is recommended that the fixture explicitly request
82-
one of the :fixture:`db`, :fixture:`transactional_db` or
83-
:fixture:`django_db_reset_sequences` fixtures. See below for a description of
84-
them.
91+
applied, depending on pytest's fixture execution order. To access the database
92+
in a fixture, it is recommended that the fixture explicitly request one of the
93+
:fixture:`db`, :fixture:`transactional_db`,
94+
:fixture:`django_db_reset_sequences` or
95+
:fixture:`django_db_serialized_rollback` fixtures. See below for a description
96+
of them.
8597

8698
.. note:: Automatic usage with ``django.test.TestCase``.
8799

@@ -331,6 +343,17 @@ fixtures which need database access themselves. A test function should normally
331343
use the :func:`pytest.mark.django_db` mark with ``transaction=True`` and
332344
``reset_sequences=True``.
333345

346+
.. fixture:: django_db_serialized_rollback
347+
348+
``django_db_serialized_rollback``
349+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
350+
351+
This fixture triggers :ref:`rollback emulation <test-case-serialized-rollback>`.
352+
This is only required for fixtures which need to enforce this behavior. A test
353+
function should normally use :func:`pytest.mark.django_db` with
354+
``serialized_rollback=True`` (and most likely also ``transaction=True``) to
355+
request this behavior.
356+
334357
.. fixture:: live_server
335358

336359
``live_server``
@@ -342,17 +365,23 @@ or by requesting it's string value: ``str(live_server)``. You can
342365
also directly concatenate a string to form a URL: ``live_server +
343366
'/foo'``.
344367

368+
Since the live server and the tests run in different threads, they
369+
cannot share a database transaction. For this reason, ``live_server``
370+
depends on the ``transactional_db`` fixture. If tests depend on data
371+
created in data migrations, you should add the
372+
``django_db_serialized_rollback`` fixture.
373+
345374
.. note:: Combining database access fixtures.
346375

347376
When using multiple database fixtures together, only one of them is
348377
used. Their order of precedence is as follows (the last one wins):
349378

350379
* ``db``
351380
* ``transactional_db``
352-
* ``django_db_reset_sequences``
353381

354-
In addition, using ``live_server`` will also trigger transactional
355-
database access, if not specified.
382+
In addition, using ``live_server`` or ``django_db_reset_sequences`` will also
383+
trigger transactional database access, and ``django_db_serialized_rollback``
384+
regular database access, if not specified.
356385

357386
.. fixture:: settings
358387

pytest_django/fixtures.py

+47-7
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@
2020
import django
2121

2222
_DjangoDbDatabases = Optional[Union["Literal['__all__']", Iterable[str]]]
23-
_DjangoDb = Tuple[bool, bool, _DjangoDbDatabases]
23+
# transaction, reset_sequences, databases, serialized_rollback
24+
_DjangoDb = Tuple[bool, bool, _DjangoDbDatabases, bool]
2425

2526

2627
__all__ = [
2728
"django_db_setup",
2829
"db",
2930
"transactional_db",
3031
"django_db_reset_sequences",
32+
"django_db_serialized_rollback",
3133
"admin_user",
3234
"django_user_model",
3335
"django_username_field",
@@ -151,9 +153,19 @@ def _django_db_helper(
151153

152154
marker = request.node.get_closest_marker("django_db")
153155
if marker:
154-
transactional, reset_sequences, databases = validate_django_db(marker)
156+
(
157+
transactional,
158+
reset_sequences,
159+
databases,
160+
serialized_rollback,
161+
) = validate_django_db(marker)
155162
else:
156-
transactional, reset_sequences, databases = False, False, None
163+
(
164+
transactional,
165+
reset_sequences,
166+
databases,
167+
serialized_rollback,
168+
) = False, False, None, False
157169

158170
transactional = transactional or (
159171
"transactional_db" in request.fixturenames
@@ -162,6 +174,9 @@ def _django_db_helper(
162174
reset_sequences = reset_sequences or (
163175
"django_db_reset_sequences" in request.fixturenames
164176
)
177+
serialized_rollback = serialized_rollback or (
178+
"django_db_serialized_rollback" in request.fixturenames
179+
)
165180

166181
django_db_blocker.unblock()
167182
request.addfinalizer(django_db_blocker.restore)
@@ -175,10 +190,12 @@ def _django_db_helper(
175190
test_case_class = django.test.TestCase
176191

177192
_reset_sequences = reset_sequences
193+
_serialized_rollback = serialized_rollback
178194
_databases = databases
179195

180196
class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type]
181197
reset_sequences = _reset_sequences
198+
serialized_rollback = _serialized_rollback
182199
if _databases is not None:
183200
databases = _databases
184201

@@ -196,18 +213,20 @@ def validate_django_db(marker) -> "_DjangoDb":
196213
"""Validate the django_db marker.
197214
198215
It checks the signature and creates the ``transaction``,
199-
``reset_sequences`` and ``databases`` attributes on the marker
200-
which will have the correct values.
216+
``reset_sequences``, ``databases`` and ``serialized_rollback`` attributes on
217+
the marker which will have the correct values.
201218
202-
A sequence reset is only allowed when combined with a transaction.
219+
Sequence reset and serialized_rollback are only allowed when combined with
220+
transaction.
203221
"""
204222

205223
def apifun(
206224
transaction: bool = False,
207225
reset_sequences: bool = False,
208226
databases: "_DjangoDbDatabases" = None,
227+
serialized_rollback: bool = False,
209228
) -> "_DjangoDb":
210-
return transaction, reset_sequences, databases
229+
return transaction, reset_sequences, databases, serialized_rollback
211230

212231
return apifun(*marker.args, **marker.kwargs)
213232

@@ -303,6 +322,27 @@ def django_db_reset_sequences(
303322
# is requested.
304323

305324

325+
@pytest.fixture(scope="function")
326+
def django_db_serialized_rollback(
327+
_django_db_helper: None,
328+
db: None,
329+
) -> None:
330+
"""Require a test database with serialized rollbacks.
331+
332+
This requests the ``db`` fixture, and additionally performs rollback
333+
emulation - serializes the database contents during setup and restores
334+
it during teardown.
335+
336+
This fixture may be useful for transactional tests, so is usually combined
337+
with ``transactional_db``, but can also be useful on databases which do not
338+
support transactions.
339+
340+
Note that this will slow down that test suite by approximately 3x.
341+
"""
342+
# The `_django_db_helper` fixture checks if `django_db_serialized_rollback`
343+
# is requested.
344+
345+
306346
@pytest.fixture()
307347
def client() -> "django.test.client.Client":
308348
"""A Django test client instance."""

pytest_django/plugin.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from .fixtures import django_db_modify_db_settings_tox_suffix # noqa
3434
from .fixtures import django_db_modify_db_settings_xdist_suffix # noqa
3535
from .fixtures import django_db_reset_sequences # noqa
36+
from .fixtures import django_db_serialized_rollback # noqa
3637
from .fixtures import django_db_setup # noqa
3738
from .fixtures import django_db_use_migrations # noqa
3839
from .fixtures import django_user_model # noqa
@@ -265,14 +266,17 @@ def pytest_load_initial_conftests(
265266
# Register the marks
266267
early_config.addinivalue_line(
267268
"markers",
268-
"django_db(transaction=False, reset_sequences=False, databases=None): "
269+
"django_db(transaction=False, reset_sequences=False, databases=None, "
270+
"serialized_rollback=False): "
269271
"Mark the test as using the Django test database. "
270272
"The *transaction* argument allows you to use real transactions "
271273
"in the test like Django's TransactionTestCase. "
272274
"The *reset_sequences* argument resets database sequences before "
273275
"the test. "
274276
"The *databases* argument sets which database aliases the test "
275-
"uses (by default, only 'default'). Use '__all__' for all databases.",
277+
"uses (by default, only 'default'). Use '__all__' for all databases. "
278+
"The *serialized_rollback* argument enables rollback emulation for "
279+
"the test.",
276280
)
277281
early_config.addinivalue_line(
278282
"markers",
@@ -387,7 +391,12 @@ def get_order_number(test: pytest.Item) -> int:
387391
else:
388392
marker_db = test.get_closest_marker('django_db')
389393
if marker_db:
390-
transaction, reset_sequences, databases = validate_django_db(marker_db)
394+
(
395+
transaction,
396+
reset_sequences,
397+
databases,
398+
serialized_rollback,
399+
) = validate_django_db(marker_db)
391400
uses_db = True
392401
transactional = transaction or reset_sequences
393402
else:

tests/test_database.py

+82-1
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,23 @@ def non_zero_sequences_counter(db: None) -> None:
4848
class TestDatabaseFixtures:
4949
"""Tests for the different database fixtures."""
5050

51-
@pytest.fixture(params=["db", "transactional_db", "django_db_reset_sequences"])
51+
@pytest.fixture(params=[
52+
"db",
53+
"transactional_db",
54+
"django_db_reset_sequences",
55+
"django_db_serialized_rollback",
56+
])
5257
def all_dbs(self, request) -> None:
5358
if request.param == "django_db_reset_sequences":
5459
return request.getfixturevalue("django_db_reset_sequences")
5560
elif request.param == "transactional_db":
5661
return request.getfixturevalue("transactional_db")
5762
elif request.param == "db":
5863
return request.getfixturevalue("db")
64+
elif request.param == "django_db_serialized_rollback":
65+
return request.getfixturevalue("django_db_serialized_rollback")
66+
else:
67+
assert False # pragma: no cover
5968

6069
def test_access(self, all_dbs: None) -> None:
6170
Item.objects.create(name="spam")
@@ -113,6 +122,51 @@ def test_django_db_reset_sequences_requested(
113122
["*test_django_db_reset_sequences_requested PASSED*"]
114123
)
115124

125+
def test_serialized_rollback(self, db: None, django_testdir) -> None:
126+
django_testdir.create_app_file(
127+
"""
128+
from django.db import migrations
129+
130+
def load_data(apps, schema_editor):
131+
Item = apps.get_model("app", "Item")
132+
Item.objects.create(name="loaded-in-migration")
133+
134+
class Migration(migrations.Migration):
135+
dependencies = [
136+
("app", "0001_initial"),
137+
]
138+
139+
operations = [
140+
migrations.RunPython(load_data),
141+
]
142+
""",
143+
"migrations/0002_data_migration.py",
144+
)
145+
146+
django_testdir.create_test_module(
147+
"""
148+
import pytest
149+
from .app.models import Item
150+
151+
@pytest.mark.django_db(transaction=True, serialized_rollback=True)
152+
def test_serialized_rollback_1():
153+
assert Item.objects.filter(name="loaded-in-migration").exists()
154+
155+
@pytest.mark.django_db(transaction=True)
156+
def test_serialized_rollback_2(django_db_serialized_rollback):
157+
assert Item.objects.filter(name="loaded-in-migration").exists()
158+
Item.objects.create(name="test2")
159+
160+
@pytest.mark.django_db(transaction=True, serialized_rollback=True)
161+
def test_serialized_rollback_3():
162+
assert Item.objects.filter(name="loaded-in-migration").exists()
163+
assert not Item.objects.filter(name="test2").exists()
164+
"""
165+
)
166+
167+
result = django_testdir.runpytest_subprocess("-v")
168+
assert result.ret == 0
169+
116170
@pytest.fixture
117171
def mydb(self, all_dbs: None) -> None:
118172
# This fixture must be able to access the database
@@ -160,6 +214,10 @@ def fixture_with_transdb(self, transactional_db: None) -> None:
160214
def fixture_with_reset_sequences(self, django_db_reset_sequences: None) -> None:
161215
Item.objects.create(name="spam")
162216

217+
@pytest.fixture
218+
def fixture_with_serialized_rollback(self, django_db_serialized_rollback: None) -> None:
219+
Item.objects.create(name="ham")
220+
163221
def test_trans(self, fixture_with_transdb: None) -> None:
164222
pass
165223

@@ -180,6 +238,16 @@ def test_reset_sequences(
180238
) -> None:
181239
pass
182240

241+
# The test works when transactions are not supported, but it interacts
242+
# badly with other tests.
243+
@pytest.mark.skipif('not connection.features.supports_transactions')
244+
def test_serialized_rollback(
245+
self,
246+
fixture_with_serialized_rollback: None,
247+
fixture_with_db: None,
248+
) -> None:
249+
pass
250+
183251

184252
class TestDatabaseMarker:
185253
"Tests for the django_db marker."
@@ -264,6 +332,19 @@ def test_all_databases(self, request) -> None:
264332
SecondItem.objects.count()
265333
SecondItem.objects.create(name="spam")
266334

335+
@pytest.mark.django_db
336+
def test_serialized_rollback_disabled(self, request):
337+
marker = request.node.get_closest_marker("django_db")
338+
assert not marker.kwargs
339+
340+
# The test works when transactions are not supported, but it interacts
341+
# badly with other tests.
342+
@pytest.mark.skipif('not connection.features.supports_transactions')
343+
@pytest.mark.django_db(serialized_rollback=True)
344+
def test_serialized_rollback_enabled(self, request):
345+
marker = request.node.get_closest_marker("django_db")
346+
assert marker.kwargs["serialized_rollback"]
347+
267348

268349
def test_unittest_interaction(django_testdir) -> None:
269350
"Test that (non-Django) unittests cannot access the DB."

0 commit comments

Comments
 (0)