Skip to content

Commit db26027

Browse files
cb109Oliver Sauder
authored and
Oliver Sauder
committed
Support reset sequences
This adds support for using the reset_sequences feature of Django's TransactionTestCase. It will try to reset all automatic increment values before test execution, if the database supports it. This is useful for when you have tests that rely on such values, like ids or other primary keys.
1 parent db44ccf commit db26027

File tree

4 files changed

+173
-43
lines changed

4 files changed

+173
-43
lines changed

docs/helpers.rst

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ on what marks are and for notes on using_ them.
1313
.. _using: https://pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules
1414

1515

16-
``pytest.mark.django_db(transaction=False)`` - request database access
17-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16+
``pytest.mark.django_db`` - request database access
17+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1818

19-
.. :py:function:: pytest.mark.django_db:
19+
.. :py:function:: pytest.mark.django_db([transaction=False, reset_sequences=False]):
2020
2121
This is used to mark a test function as requiring the database. It
2222
will ensure the database is set up correctly for the test. Each test
@@ -25,9 +25,9 @@ of the test. This behavior is the same as Django's standard
2525
`django.test.TestCase`_ class.
2626

2727
In order for a test to have access to the database it must either
28-
be marked using the ``django_db`` mark or request one of the ``db``
29-
or ``transactional_db`` fixtures. Otherwise the test will fail
30-
when trying to access the database.
28+
be marked using the ``django_db`` mark or request one of the ``db``,
29+
``transactional_db`` or ``reset_sequences_db`` fixtures. Otherwise the
30+
test will fail when trying to access the database.
3131

3232
:type transaction: bool
3333
:param transaction:
@@ -38,14 +38,23 @@ when trying to access the database.
3838
uses. When ``transaction=True``, the behavior will be the same as
3939
`django.test.TransactionTestCase`_
4040

41+
42+
:type reset_sequences: bool
43+
:param reset_sequences:
44+
The ``reset_sequences`` argument will ask to reset auto increment sequence
45+
values (e.g. primary keys) before running the test. Defaults to
46+
``False``. Must be used together with ``transaction=True`` to have an
47+
effect. Please be aware that not all databases support this feature.
48+
For details see `django.test.TransactionTestCase.reset_sequences`_
49+
4150
.. note::
4251

4352
If you want access to the Django database *inside a fixture*
4453
this marker will not help even if the function requesting your
4554
fixture has this marker applied. To access the database in a
46-
fixture, the fixture itself will have to request the ``db`` or
47-
``transactional_db`` fixture. See below for a description of
48-
them.
55+
fixture, the fixture itself will have to request the ``db``,
56+
``transactional_db`` or ``reset_sequences_db`` fixture. See below
57+
for a description of them.
4958

5059
.. note:: Automatic usage with ``django.test.TestCase``.
5160

@@ -54,6 +63,7 @@ when trying to access the database.
5463
Test classes that subclass Python's ``unittest.TestCase`` need to have the
5564
marker applied in order to access the database.
5665

66+
.. _django.test.TransactionTestCase.reset_sequences: https://docs.djangoproject.com/en/dev/topics/testing/advanced/#django.test.TransactionTestCase.reset_sequences
5767
.. _django.test.TestCase: https://docs.djangoproject.com/en/dev/topics/testing/overview/#testcase
5868
.. _django.test.TransactionTestCase: https://docs.djangoproject.com/en/dev/topics/testing/overview/#transactiontestcase
5969

@@ -215,8 +225,17 @@ mark to signal it needs the database.
215225

216226
This fixture can be used to request access to the database including
217227
transaction support. This is only required for fixtures which need
218-
database access themselves. A test function would normally use the
219-
``pytest.mark.django_db`` mark to signal it needs the database.
228+
database access themselves. A test function should normally use the
229+
``pytest.mark.django_db`` mark with ``transaction=True``.
230+
231+
``reset_sequences_db``
232+
~~~~~~~~~~~~~~~~~~~~~~
233+
234+
This fixture provides the same transactional database access as
235+
``transactional_db``, with additional support for reset of auto increment
236+
sequences (if your database supports it). This is only required for
237+
fixtures which need database access themselves. A test function should
238+
normally use the ``pytest.mark.django_db`` mark with ``transaction=True`` and ``reset_sequences=True``.
220239

221240
``live_server``
222241
~~~~~~~~~~~~~~~
@@ -227,6 +246,18 @@ or by requesting it's string value: ``unicode(live_server)``. You can
227246
also directly concatenate a string to form a URL: ``live_server +
228247
'/foo``.
229248

249+
.. note:: Combining database access fixtures.
250+
251+
When using multiple database fixtures together, only one of them is
252+
used. Their order of precedence is as follows (the last one wins):
253+
254+
* ``db``
255+
* ``transactional_db``
256+
* ``reset_sequences_db``
257+
258+
In addition, using ``live_server`` will also trigger transactional
259+
database access, if not specified.
260+
230261
``settings``
231262
~~~~~~~~~~~~
232263

pytest_django/fixtures.py

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414

1515
from .lazy_django import skip_if_no_django
1616

17-
__all__ = ['django_db_setup', 'db', 'transactional_db', 'admin_user',
18-
'django_user_model', 'django_username_field',
17+
__all__ = ['django_db_setup', 'db', 'transactional_db', 'django_db_reset_sequences',
18+
'admin_user', 'django_user_model', 'django_username_field',
1919
'client', 'admin_client', 'rf', 'settings', 'live_server',
2020
'_live_server_helper', 'django_assert_num_queries']
2121

@@ -107,7 +107,8 @@ def teardown_database():
107107
request.addfinalizer(teardown_database)
108108

109109

110-
def _django_db_fixture_helper(transactional, request, django_db_blocker):
110+
def _django_db_fixture_helper(request, django_db_blocker,
111+
transactional=False, reset_sequences=False):
111112
if is_django_unittest(request):
112113
return
113114

@@ -120,6 +121,11 @@ def _django_db_fixture_helper(transactional, request, django_db_blocker):
120121

121122
if transactional:
122123
from django.test import TransactionTestCase as django_case
124+
125+
if reset_sequences:
126+
class ResetSequenceTestCase(django_case):
127+
reset_sequences = True
128+
django_case = ResetSequenceTestCase
123129
else:
124130
from django.test import TestCase as django_case
125131

@@ -139,7 +145,7 @@ def _disable_native_migrations():
139145

140146
@pytest.fixture(scope='function')
141147
def db(request, django_db_setup, django_db_blocker):
142-
"""Require a django test database
148+
"""Require a django test database.
143149
144150
This database will be setup with the default fixtures and will have
145151
the transaction management disabled. At the end of the test the outer
@@ -148,30 +154,54 @@ def db(request, django_db_setup, django_db_blocker):
148154
This is more limited than the ``transactional_db`` resource but
149155
faster.
150156
151-
If both this and ``transactional_db`` are requested then the
152-
database setup will behave as only ``transactional_db`` was
153-
requested.
157+
If multiple database fixtures are requested, they take precedence
158+
over each other in the following order (the last one wins): ``db``,
159+
``transactional_db``, ``django_db_reset_sequences``.
154160
"""
161+
if 'django_db_reset_sequences' in request.funcargnames:
162+
request.getfixturevalue('django_db_reset_sequences')
155163
if 'transactional_db' in request.funcargnames \
156164
or 'live_server' in request.funcargnames:
157165
request.getfixturevalue('transactional_db')
158166
else:
159-
_django_db_fixture_helper(False, request, django_db_blocker)
167+
_django_db_fixture_helper(request, django_db_blocker, transactional=False)
160168

161169

162170
@pytest.fixture(scope='function')
163171
def transactional_db(request, django_db_setup, django_db_blocker):
164-
"""Require a django test database with transaction support
172+
"""Require a django test database with transaction support.
165173
166174
This will re-initialise the django database for each test and is
167175
thus slower than the normal ``db`` fixture.
168176
169177
If you want to use the database with transactions you must request
170-
this resource. If both this and ``db`` are requested then the
171-
database setup will behave as only ``transactional_db`` was
172-
requested.
178+
this resource.
179+
180+
If multiple database fixtures are requested, they take precedence
181+
over each other in the following order (the last one wins): ``db``,
182+
``transactional_db``, ``django_db_reset_sequences``.
183+
"""
184+
if 'django_db_reset_sequences' in request.funcargnames:
185+
request.getfuncargvalue('django_db_reset_sequences')
186+
_django_db_fixture_helper(request, django_db_blocker,
187+
transactional=True)
188+
189+
190+
@pytest.fixture(scope='function')
191+
def django_db_reset_sequences(request, django_db_setup, django_db_blocker):
192+
"""Require a transactional test database with sequence reset support.
193+
194+
This behaves like the ``transactional_db`` fixture, with the addition
195+
of enforcing a reset of all auto increment sequences. If the enquiring
196+
test relies on such values (e.g. ids as primary keys), you should
197+
request this resource to ensure they are consistent across tests.
198+
199+
If multiple database fixtures are requested, they take precedence
200+
over each other in the following order (the last one wins): ``db``,
201+
``transactional_db``, ``django_db_reset_sequences``.
173202
"""
174-
_django_db_fixture_helper(True, request, django_db_blocker)
203+
_django_db_fixture_helper(request, django_db_blocker,
204+
transactional=True, reset_sequences=True)
175205

176206

177207
@pytest.fixture()

pytest_django/plugin.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from .fixtures import django_user_model # noqa
3131
from .fixtures import django_username_field # noqa
3232
from .fixtures import live_server # noqa
33+
from .fixtures import django_db_reset_sequences # noqa
3334
from .fixtures import rf # noqa
3435
from .fixtures import settings # noqa
3536
from .fixtures import transactional_db # noqa
@@ -379,13 +380,15 @@ def django_db_blocker():
379380
def _django_db_marker(request):
380381
"""Implement the django_db marker, internal to pytest-django.
381382
382-
This will dynamically request the ``db`` or ``transactional_db``
383-
fixtures as required by the django_db marker.
383+
This will dynamically request the ``db``, ``transactional_db`` or
384+
``django_db_reset_sequences`` fixtures as required by the django_db marker.
384385
"""
385386
marker = request.node.get_closest_marker('django_db')
386387
if marker:
387-
transaction = validate_django_db(marker)
388-
if transaction:
388+
transaction, reset_sequences = validate_django_db(marker)
389+
if reset_sequences:
390+
request.getfixturevalue('django_db_reset_sequences')
391+
elif transaction:
389392
request.getfixturevalue('transactional_db')
390393
else:
391394
request.getfixturevalue('db')
@@ -667,11 +670,14 @@ def restore(self):
667670
def validate_django_db(marker):
668671
"""Validate the django_db marker.
669672
670-
It checks the signature and creates the `transaction` attribute on
671-
the marker which will have the correct value.
673+
It checks the signature and creates the ``transaction`` and
674+
``reset_sequences`` attributes on the marker which will have the
675+
correct values.
676+
677+
A sequence reset is only allowed when combined with a transaction.
672678
"""
673-
def apifun(transaction=False):
674-
return transaction
679+
def apifun(transaction=False, reset_sequences=False):
680+
return transaction, reset_sequences
675681
return apifun(*marker.args, **marker.kwargs)
676682

677683

tests/test_database.py

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
from pytest_django_test.app.models import Item
88

99

10+
def db_supports_reset_sequences():
11+
"""Return if the current db engine supports `reset_sequences`."""
12+
return (connection.features.supports_transactions and
13+
connection.features.supports_sequence_reset)
14+
15+
1016
def test_noaccess():
1117
with pytest.raises(pytest.fail.Exception):
1218
Item.objects.create(name='spam')
@@ -27,20 +33,34 @@ def test_noaccess_fixture(noaccess):
2733
pass
2834

2935

30-
class TestDatabaseFixtures:
31-
"""Tests for the db and transactional_db fixtures"""
36+
@pytest.fixture
37+
def non_zero_sequences_counter(db):
38+
"""Ensure that the db's internal sequence counter is > 1.
39+
40+
This is used to test the `reset_sequences` feature.
41+
"""
42+
item_1 = Item.objects.create(name='item_1')
43+
item_2 = Item.objects.create(name='item_2')
44+
item_1.delete()
45+
item_2.delete()
46+
3247

33-
@pytest.fixture(params=['db', 'transactional_db'])
34-
def both_dbs(self, request):
35-
if request.param == 'transactional_db':
36-
return request.getfixturevalue('transactional_db')
48+
class TestDatabaseFixtures:
49+
"""Tests for the different database fixtures."""
50+
51+
@pytest.fixture(params=['db', 'transactional_db', 'django_db_reset_sequences'])
52+
def all_dbs(self, request):
53+
if request.param == 'django_db_reset_sequences':
54+
return request.getfuncargvalue('django_db_reset_sequences')
55+
elif request.param == 'transactional_db':
56+
return request.getfuncargvalue('transactional_db')
3757
elif request.param == 'db':
3858
return request.getfixturevalue('db')
3959

40-
def test_access(self, both_dbs):
60+
def test_access(self, all_dbs):
4161
Item.objects.create(name='spam')
4262

43-
def test_clean_db(self, both_dbs):
63+
def test_clean_db(self, all_dbs):
4464
# Relies on the order: test_access created an object
4565
assert Item.objects.count() == 0
4666

@@ -56,8 +76,39 @@ def test_transactions_enabled(self, transactional_db):
5676

5777
assert not connection.in_atomic_block
5878

79+
def test_transactions_enabled_via_reset_seq(
80+
self, django_db_reset_sequences):
81+
if not connections_support_transactions():
82+
pytest.skip('transactions required for this test')
83+
84+
assert not connection.in_atomic_block
85+
86+
def test_django_db_reset_sequences_fixture(
87+
self, db, django_testdir, non_zero_sequences_counter):
88+
89+
if not db_supports_reset_sequences():
90+
pytest.skip('transactions and reset_sequences must be supported '
91+
'by the database to run this test')
92+
93+
# The test runs on a database that already contains objects, so its
94+
# id counter is > 1. We check for the ids of newly created objects.
95+
django_testdir.create_test_module('''
96+
import pytest
97+
from .app.models import Item
98+
99+
def test_django_db_reset_sequences_requested(
100+
django_db_reset_sequences):
101+
item = Item.objects.create(name='new_item')
102+
assert item.id == 1
103+
''')
104+
105+
result = django_testdir.runpytest_subprocess('-v', '--reuse-db')
106+
result.stdout.fnmatch_lines([
107+
"*test_django_db_reset_sequences_requested PASSED*",
108+
])
109+
59110
@pytest.fixture
60-
def mydb(self, both_dbs):
111+
def mydb(self, all_dbs):
61112
# This fixture must be able to access the database
62113
Item.objects.create(name='spam')
63114

@@ -69,13 +120,13 @@ def test_mydb(self, mydb):
69120
item = Item.objects.get(name='spam')
70121
assert item
71122

72-
def test_fixture_clean(self, both_dbs):
123+
def test_fixture_clean(self, all_dbs):
73124
# Relies on the order: test_mydb created an object
74125
# See https://github.com/pytest-dev/pytest-django/issues/17
75126
assert Item.objects.count() == 0
76127

77128
@pytest.fixture
78-
def fin(self, request, both_dbs):
129+
def fin(self, request, all_dbs):
79130
# This finalizer must be able to access the database
80131
request.addfinalizer(lambda: Item.objects.create(name='spam'))
81132

@@ -139,6 +190,18 @@ def test_transactions_enabled(self):
139190

140191
assert not connection.in_atomic_block
141192

193+
@pytest.mark.django_db
194+
def test_reset_sequences_disabled(self, request):
195+
marker = request.keywords['django_db']
196+
197+
assert not marker.kwargs
198+
199+
@pytest.mark.django_db(reset_sequences=True)
200+
def test_reset_sequences_enabled(self, request):
201+
marker = request.keywords['django_db']
202+
203+
assert marker.kwargs['reset_sequences']
204+
142205

143206
def test_unittest_interaction(django_testdir):
144207
"Test that (non-Django) unittests cannot access the DB."

0 commit comments

Comments
 (0)