Skip to content

Commit e540ace

Browse files
authored
Database fixture refactor (#362)
1 parent def2882 commit e540ace

File tree

7 files changed

+419
-160
lines changed

7 files changed

+419
-160
lines changed

docs/changelog.rst

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ Features
1515
* Added a new option `--migrations` to negate a default usage of
1616
`--nomigrations`.
1717

18+
* The previously internal pytest-django fixture that handles database creation
19+
and setup has been refactored, refined and made a public API.
20+
21+
This opens up more flexibility and advanced use cases to configure the test
22+
database in new ways.
23+
24+
See :ref:`advanced-database-configuration` for more information on the new
25+
fixtures and example use cases.
26+
1827
Compatibility
1928
^^^^^^^^^^^^^
2029
* Django versions 1.4, 1.5 and 1.6 is no longer supported. The supported
@@ -29,9 +38,15 @@ Compatibility
2938
database access was prevented on the cursor level. To be safer and prevent
3039
more cases, it is now prevented at the connection level. If you previously
3140
had tests which interacted with the databases without a database cursor, you
32-
will need to mark them with the `pytest.mark.django_db` marker or request the
41+
will need to mark them with the :func:`pytest.mark.django_db` marker or request the
3342
`db` fixture.
3443

44+
* The previously undocumented internal fixtures ``_django_db_setup``,
45+
``_django_cursor_wrapper`` has been removed in favour of the new public
46+
fixtures. If you previously relied on these internal fixtures, you must
47+
update your code. See :ref:`advanced-database-configuration` for more
48+
information on the new fixtures and example use cases.
49+
3550
2.9.1
3651
-----
3752

docs/database.rst

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,275 @@ Using ``--nomigrations`` will disable Django migrations and create the database
128128
by inspecting all models. It may be faster when there are several migrations to
129129
run in the database setup. You can use ``--migrations`` to force running
130130
migrations in case ``--nomigrations`` is used, e.g. in ``setup.cfg``.
131+
132+
.. _advanced-database-configuration:
133+
134+
Advanced database configuration
135+
-------------------------------
136+
137+
pytest-django provides options to customize the way database is configured. The
138+
default database construction mostly follows Django's own test runner. You can
139+
however influence all parts of the database setup process to make it fit in
140+
projects with special requirements.
141+
142+
This section assumes some familiary with the Django test runner, Django
143+
database creation and pytest fixtures.
144+
145+
Fixtures
146+
########
147+
148+
There are some fixtures which will let you change the way the database is
149+
configured in your own project. These fixtures can be overridden in your own
150+
project by specifying a fixture with the same name and scope in ``conftest.py``.
151+
152+
.. admonition:: Use the pytest-django source code
153+
154+
The default implementation of these fixtures can be found in
155+
`fixtures.py <https://github.com/pytest-dev/pytest-django/blob/master/pytest_django/fixtures.py>`_.
156+
157+
The code is relatively short and straightforward and can provide a
158+
starting point when you need to customize database setup in your own
159+
project.
160+
161+
162+
django_db_setup
163+
"""""""""""""""
164+
165+
.. fixture:: django_db_setup
166+
167+
This is the top-level fixture that ensures that the test databases are created
168+
and available. This fixture is session scoped (it will be run once per test
169+
session) and is responsible for making sure the test database is available for tests
170+
that need it.
171+
172+
The default implementation creates the test database by applying migrations and removes
173+
databases after the test run.
174+
175+
You can override this fixture in your own ``conftest.py`` to customize how test
176+
databases are constructed.
177+
178+
django_db_modify_db_settings
179+
""""""""""""""""""""""""""""
180+
181+
.. fixture:: django_db_modify_db_settings
182+
183+
This fixture allows modifying `django.conf.settings.DATABASES` just before the
184+
databases are configured.
185+
186+
If you need to customize the location of your test database, this is the
187+
fixture you want to override.
188+
189+
The default implementation of this fixture requests the
190+
:fixture:`django_db_modify_db_settings_xdist_suffix` to provide compatibility
191+
with pytest-xdist.
192+
193+
This fixture is by default requested from :fixture:`django_db_setup`.
194+
195+
django_db_modify_db_settings_xdist_suffix
196+
"""""""""""""""""""""""""""""""""""""""""
197+
198+
.. fixture:: django_db_modify_db_settings_xdist_suffix
199+
200+
Requesting this fixture will add a suffix to the database name when the tests
201+
are run via pytest-xdist.
202+
203+
This fixture is by default requsted from
204+
:fixture:`django_db_modify_db_settings_xdist_suffix`.
205+
206+
django_db_use_migrations
207+
""""""""""""""""""""""""
208+
209+
.. fixture:: django_db_use_migrations
210+
211+
Returns whether or not to use migrations to create the test
212+
databases.
213+
214+
The default implementation returns the value of the
215+
``--migrations``/``--nomigrations`` command line options.
216+
217+
This fixture is by default requested from :fixture:`django_db_setup`.
218+
219+
django_db_keepdb
220+
""""""""""""""""
221+
222+
.. fixture:: django_db_keepdb
223+
224+
Returns whether or not to re-use an existing database and to keep it after the
225+
test run.
226+
227+
The default implementation handles the ``--reuse-db`` and ``--create-db``
228+
command line options.
229+
230+
This fixture is by default requested from :fixture:`django_db_setup`.
231+
232+
django_db_blocker
233+
"""""""""""""""""
234+
235+
.. fixture:: django_db_blocker
236+
237+
.. warning::
238+
It does not manage transactions and changes made to the database will not
239+
be automatically restored. Using the :func:`pytest.mark.django_db` marker
240+
or :fixture:`db` fixture, which wraps database changes in a transaction and
241+
restores the state is generally the thing you want in tests. This marker
242+
can be used when you are trying to influence the way the database is
243+
configured.
244+
245+
Database access is by default not allowed. ``django_db_blocker`` is the object
246+
which can allow specific code paths to have access to the database. This
247+
fixture is used internally to implement the ``db`` fixture.
248+
249+
250+
:fixture:`django_db_blocker` can be used as a context manager to enable database
251+
access for the specified block::
252+
253+
@pytest.fixture
254+
def myfixture(django_db_blocker):
255+
with django_db_blocker:
256+
... # modify something in the database
257+
258+
You can also manage the access manually via these methods:
259+
260+
.. py:method:: django_db_blocker.enable_database_access()
261+
262+
Enable database access. Should be followed by a call to
263+
:func:`~django_db_blocker.restore_previous_access`.
264+
265+
.. py:method:: django_db_blocker.disable_database_access()
266+
267+
Disable database access. Should be followed by a call to
268+
:func:`~django_db_blocker.restore_previous_access`.
269+
270+
.. py:function:: django_db_blocker.restore_previous_access()
271+
272+
Restore the previous state of the database blocking.
273+
274+
Examples
275+
########
276+
277+
Using a template database for tests
278+
"""""""""""""""""""""""""""""""""""
279+
280+
This example shows how a pre-created PostgreSQL source database can be copied
281+
and used for tests.
282+
283+
Put this into ``conftest.py``::
284+
285+
import pytest
286+
from django.db import connections
287+
288+
import psycopg2
289+
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
290+
291+
292+
def run_sql(sql):
293+
conn = psycopg2.connect(database='postgres')
294+
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
295+
cur = conn.cursor()
296+
cur.execute(sql)
297+
conn.close()
298+
299+
300+
@pytest.yield_fixture(scope='session')
301+
def django_db_setup():
302+
from django.conf import settings
303+
304+
settings.DATABASES['default']['NAME'] = 'the_copied_db'
305+
306+
run_sql('DROP DATABASE IF EXISTS the_copied_db')
307+
run_sql('CREATE DATABASE the_copied_db TEMPLATE the_source_db')
308+
309+
yield
310+
311+
for connection in connections.all():
312+
connection.close()
313+
314+
run_sql('DROP DATABASE the_copied_db')
315+
316+
317+
Using an existing, external database for tests
318+
""""""""""""""""""""""""""""""""""""""""""""""
319+
320+
This example shows how you can connect to an existing database and use it for
321+
your tests. This example is trivial, you just need to disable all of
322+
pytest-django and Django's test database creation and point to the existing
323+
database. This is achieved by simply implementing a no-op
324+
:fixture:`django_db_setup` fixture.
325+
326+
Put this into ``conftest.py``::
327+
328+
import pytest
329+
330+
331+
@pytest.fixture(scope='session')
332+
def django_db_setup():
333+
settings.DATABASES['default'] = {
334+
'ENGINE': 'django.db.backends.mysql',
335+
'HOST': 'db.example.com',
336+
'NAME': 'external_db',
337+
}
338+
339+
340+
Populate the database with initial test data
341+
""""""""""""""""""""""""""""""""""""""""""""
342+
343+
This example shows how you can populate the test database with test data. The
344+
test data will be saved in the database, i.e. it will not just be part of a
345+
transactions. This example uses Django's fixture loading mechanism, but it can
346+
be replaced with any way of loading data into the database.
347+
348+
Notice that :fixture:`django_db_setup` is in the argument list. This may look
349+
odd at first, but it will make sure that the sure that the original
350+
pytest-django fixture is used to create the test database. When
351+
``call_command`` is invoked, the test database is already prepared and
352+
configured.
353+
354+
Put this in ``conftest.py``::
355+
356+
import pytest
357+
358+
from django.core.management import call_command
359+
360+
@pytest.fixture(scope='session')
361+
def django_db_setup(django_db_setup, django_db_blocker):
362+
with django_db_blocker:
363+
call_command('loaddata', 'your_data_fixture.json')
364+
365+
Use the same database for all xdist processes
366+
"""""""""""""""""""""""""""""""""""""""""""""
367+
368+
By default, each xdist process gets its own database to run tests on. This is
369+
needed to have transactional tests that does not interfere with eachother.
370+
371+
If you instead want your tests to use the same database, override the
372+
:fixture:`django_db_modify_db_settings` to not do anything. Put this in
373+
``conftest.py``::
374+
375+
import pytest
376+
377+
378+
@pytest.fixture(scope='session')
379+
def django_db_modify_db_settings():
380+
pass
381+
382+
Randomize database sequences
383+
""""""""""""""""""""""""""""
384+
385+
You can customize the test database after it has been created by extending the
386+
:fixture:`django_db_setup` fixture. This example shows how to give a PostgreSQL
387+
sequence a random starting value. This can be used to detect and prevent
388+
primary key id's from being hard-coded in tests.
389+
390+
Put this in ``conftest.py``::
391+
392+
import random
393+
import pytest
394+
from django.db import connection
395+
396+
397+
@pytest.fixture(scope='session')
398+
def django_db_setup(django_db_setup, django_db_blocker):
399+
with django_db_blocker:
400+
cur = connection.cursor()
401+
cur.execute('ALTER SEQUENCE app_model_id_seq RESTART WITH %s;',
402+
[random.randint(10000, 20000)])

pytest_django/db_reuse.py

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
"""Functions to aid in preserving the test database between test runs.
2-
3-
The code in this module is heavily inspired by django-nose:
4-
https://github.com/jbalogh/django-nose/
1+
"""Functions to aid in creating, reusing and destroying Django test databases
52
"""
63
import os.path
74
import sys
@@ -36,7 +33,7 @@ def test_database_exists_from_previous_run(connection):
3633

3734

3835
def _monkeypatch(obj, method_name, new_method):
39-
assert hasattr(obj, method_name)
36+
assert hasattr(obj, method_name), method_name
4037

4138
if sys.version_info < (3, 0):
4239
wrapped_method = types.MethodType(new_method, obj, obj.__class__)
@@ -46,44 +43,6 @@ def _monkeypatch(obj, method_name, new_method):
4643
setattr(obj, method_name, wrapped_method)
4744

4845

49-
def _get_db_name(db_settings, suffix):
50-
"This provides the default test db name that Django uses."
51-
name = None
52-
try:
53-
name = db_settings['TEST']['NAME']
54-
except KeyError:
55-
pass
56-
57-
if not name:
58-
if db_settings['ENGINE'] == 'django.db.backends.sqlite3':
59-
return ':memory:'
60-
else:
61-
name = 'test_' + db_settings['NAME']
62-
63-
if suffix:
64-
name = '%s_%s' % (name, suffix)
65-
return name
66-
67-
68-
def monkey_patch_creation_for_db_suffix(suffix=None):
69-
from django.db import connections
70-
71-
if suffix is not None:
72-
def _get_test_db_name(self):
73-
"""Internal: return the name of the test DB that will be created.
74-
75-
This is only useful when called from create_test_db() and
76-
_create_test_db() and when no external munging is done with the
77-
'NAME' or 'TEST_NAME' settings.
78-
"""
79-
db_name = _get_db_name(self.connection.settings_dict, suffix)
80-
return db_name
81-
82-
for connection in connections.all():
83-
_monkeypatch(connection.creation,
84-
'_get_test_db_name', _get_test_db_name)
85-
86-
8746
def create_test_db_with_reuse(self, verbosity=1, autoclobber=False,
8847
keepdb=False, serialize=False):
8948
"""

0 commit comments

Comments
 (0)