Fixed #20579 -- Improved TransactionTestCase.available_apps.

Also moved its documentation to the 'advanced' section. It doesn't
belong to the 'overview'. Same for TransactionTestCase.reset_sequences.

When available_apps is set, after a TransactionTestCase, the database
is now totally empty. post_syncdb is fired at the beginning of the next
TransactionTestCase.

Refs #20483.
This commit is contained in:
Aymeric Augustin 2013-06-11 22:56:09 +02:00
parent 0938970491
commit 55cbd65985
4 changed files with 130 additions and 87 deletions

View File

@ -32,9 +32,10 @@ class Command(NoArgsCommand):
connection = connections[db] connection = connections[db]
verbosity = int(options.get('verbosity')) verbosity = int(options.get('verbosity'))
interactive = options.get('interactive') interactive = options.get('interactive')
# 'reset_sequences' and 'allow_cascade' are stealth options # The following are stealth options used by Django's internals.
reset_sequences = options.get('reset_sequences', True) reset_sequences = options.get('reset_sequences', True)
allow_cascade = options.get('allow_cascade', False) allow_cascade = options.get('allow_cascade', False)
inhibit_post_syncdb = options.get('inhibit_post_syncdb', False)
self.style = no_style() self.style = no_style()
@ -75,16 +76,9 @@ Are you sure you want to do this?
"Hint: Look at the output of 'django-admin.py sqlflush'. That's the SQL this command wasn't able to run.\n" "Hint: Look at the output of 'django-admin.py sqlflush'. That's the SQL this command wasn't able to run.\n"
"The full error: %s") % (connection.settings_dict['NAME'], e) "The full error: %s") % (connection.settings_dict['NAME'], e)
six.reraise(CommandError, CommandError(new_msg), sys.exc_info()[2]) six.reraise(CommandError, CommandError(new_msg), sys.exc_info()[2])
# Emit the post sync signal. This allows individual
# applications to respond as if the database had been if not inhibit_post_syncdb:
# sync'd from scratch. self.emit_post_syncdb(verbosity, interactive, db)
all_models = []
for app in models.get_apps():
all_models.extend([
m for m in models.get_models(app, include_auto_created=True)
if router.allow_syncdb(db, m)
])
emit_post_sync_signal(set(all_models), verbosity, interactive, db)
# Reinstall the initial_data fixture. # Reinstall the initial_data fixture.
if options.get('load_initial_data'): if options.get('load_initial_data'):
@ -93,3 +87,15 @@ Are you sure you want to do this?
else: else:
self.stdout.write("Flush cancelled.\n") self.stdout.write("Flush cancelled.\n")
@staticmethod
def emit_post_syncdb(verbosity, interactive, database):
# Emit the post sync signal. This allows individual applications to
# respond as if the database had been sync'd from scratch.
all_models = []
for app in models.get_apps():
all_models.extend([
m for m in models.get_models(app, include_auto_created=True)
if router.allow_syncdb(database, m)
])
emit_post_sync_signal(set(all_models), verbosity, interactive, database)

View File

@ -24,6 +24,7 @@ from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.core.handlers.wsgi import WSGIHandler from django.core.handlers.wsgi import WSGIHandler
from django.core.management import call_command from django.core.management import call_command
from django.core.management.color import no_style from django.core.management.color import no_style
from django.core.management.commands import flush
from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer, from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer,
WSGIServerException) WSGIServerException)
from django.core.urlresolvers import clear_url_caches, set_urlconf from django.core.urlresolvers import clear_url_caches, set_urlconf
@ -196,8 +197,8 @@ class SimpleTestCase(ut2.TestCase):
def _pre_setup(self): def _pre_setup(self):
"""Performs any pre-test setup. This includes: """Performs any pre-test setup. This includes:
* If the Test Case class has a 'urls' member, replace the * Creating a test client.
ROOT_URLCONF with it. * If the class has a 'urls' attribute, replace ROOT_URLCONF with it.
* Clearing the mail test outbox. * Clearing the mail test outbox.
""" """
self.client = self.client_class() self.client = self.client_class()
@ -212,6 +213,10 @@ class SimpleTestCase(ut2.TestCase):
clear_url_caches() clear_url_caches()
def _post_teardown(self): def _post_teardown(self):
"""Performs any post-test things. This includes:
* Putting back the original ROOT_URLCONF if it was changed.
"""
self._urlconf_teardown() self._urlconf_teardown()
def _urlconf_teardown(self): def _urlconf_teardown(self):
@ -732,13 +737,17 @@ class TransactionTestCase(SimpleTestCase):
def _pre_setup(self): def _pre_setup(self):
"""Performs any pre-test setup. This includes: """Performs any pre-test setup. This includes:
* Flushing the database. * If the class has an 'available_apps' attribute, restricting the app
* If the Test Case class has a 'fixtures' member, installing the cache to these applications, then firing post_syncdb -- it must run
named fixtures. with the correct set of applications for the test case.
* If the class has a 'fixtures' attribute, installing these fixtures.
""" """
super(TransactionTestCase, self)._pre_setup() super(TransactionTestCase, self)._pre_setup()
if self.available_apps is not None: if self.available_apps is not None:
cache.set_available_apps(self.available_apps) cache.set_available_apps(self.available_apps)
for db_name in self._databases_names(include_mirrors=False):
flush.Command.emit_post_syncdb(
verbosity=0, interactive=False, database=db_name)
try: try:
self._fixture_setup() self._fixture_setup()
except Exception: except Exception:
@ -782,9 +791,9 @@ class TransactionTestCase(SimpleTestCase):
def _post_teardown(self): def _post_teardown(self):
"""Performs any post-test things. This includes: """Performs any post-test things. This includes:
* Putting back the original ROOT_URLCONF if it was changed. * Flushing the contents of the database, to leave a clean slate. If
* Force closing the connection, so that the next test gets the class has an 'available_apps' attribute, post_syncdb isn't fired.
a clean cursor. * Force-closing the connection, so the next test gets a clean cursor.
""" """
try: try:
self._fixture_teardown() self._fixture_teardown()
@ -801,12 +810,14 @@ class TransactionTestCase(SimpleTestCase):
cache.unset_available_apps() cache.unset_available_apps()
def _fixture_teardown(self): def _fixture_teardown(self):
# Allow TRUNCATE ... CASCADE when flushing only a subset of the apps # Allow TRUNCATE ... CASCADE and don't emit the post_syncdb signal
allow_cascade = self.available_apps is not None # when flushing only a subset of the apps
for db_name in self._databases_names(include_mirrors=False): for db_name in self._databases_names(include_mirrors=False):
call_command('flush', verbosity=0, interactive=False, call_command('flush', verbosity=0, interactive=False,
database=db_name, skip_validation=True, database=db_name, skip_validation=True,
reset_sequences=False, allow_cascade=allow_cascade) reset_sequences=False,
allow_cascade=self.available_apps is not None,
inhibit_post_syncdb=self.available_apps is not None)
def assertQuerysetEqual(self, qs, values, transform=repr, ordered=True): def assertQuerysetEqual(self, qs, values, transform=repr, ordered=True):
items = six.moves.map(transform, qs) items = six.moves.map(transform, qs)

View File

@ -155,6 +155,80 @@ If there are any circular dependencies in the
:setting:`TEST_DEPENDENCIES` definition, an ``ImproperlyConfigured`` :setting:`TEST_DEPENDENCIES` definition, an ``ImproperlyConfigured``
exception will be raised. exception will be raised.
Advanced features of ``TransactionTestCase``
============================================
.. currentmodule:: django.test
.. attribute:: TransactionTestCase.available_apps
.. versionadded:: 1.6
.. warning::
This attribute is a private API. It may be changed or removed without
a deprecation period in the future, for instance to accomodate changes
in application loading.
It's used to optimize Django's own test suite, which contains hundreds
of models but no relations between models in different applications.
By default, ``available_apps`` is set to ``None``. After each test, Django
calls :djadmin:`flush` to reset the database state. This empties all tables
and emits the :data:`~django.db.models.signals.post_syncdb` signal, which
re-creates one content type and three permissions for each model. This
operation gets expensive proportionally to the number of models.
Setting ``available_apps`` to a list of applications instructs Django to
behave as if only the models from these applications were available. The
behavior of ``TransactionTestCase`` changes as follows:
- :data:`~django.db.models.signals.post_syncdb` is fired before each
test to create the content types and permissions for each model in
available apps, in case they're missing.
- After each test, Django empties only tables corresponding to models in
available apps. However, at the database level, truncation may cascade to
related models in unavailable apps. Furthermore
:data:`~django.db.models.signals.post_syncdb` isn't fired; it will be
fired by the next ``TransactionTestCase``, after the correct set of
applications is selected.
Since the database isn't fully flushed, if a test creates instances of
models not included in ``available_apps``, they will leak and they may
cause unrelated tests to fail. Be careful with tests that use sessions;
the default session engine stores them in the database.
Since :data:`~django.db.models.signals.post_syncdb` isn't emitted after
flushing the database, its state after a ``TransactionTestCase`` isn't the
same as after a ``TestCase``: it's missing the rows created by listeners
to :data:`~django.db.models.signals.post_syncdb`. Considering the
:ref:`order in which tests are executed <order-of-tests>`, this isn't an
issue, provided either all ``TransactionTestCase`` in a given test suite
declare ``available_apps``, or none of them.
``available_apps`` is mandatory in Django's own test suite.
.. attribute:: TransactionTestCase.reset_sequences
.. versionadded:: 1.5
Setting ``reset_sequences = True`` on a ``TransactionTestCase`` will make
sure sequences are always reset before the test run::
class TestsThatDependsOnPrimaryKeySequences(TransactionTestCase):
reset_sequences = True
def test_animal_pk(self):
lion = Animal.objects.create(name="lion", sound="roar")
# lion.pk is guaranteed to always be 1
self.assertEqual(lion.pk, 1)
Unless you are explicitly testing primary keys sequence numbers, it is
recommended that you do not hard code primary key values in tests.
Using ``reset_sequences = True`` will slow down the test, since the primary
key reset is an relatively expensive database operation.
Running tests outside the test runner Running tests outside the test runner
===================================== =====================================

View File

@ -213,6 +213,8 @@ advanced settings.
The :ref:`advanced multi-db testing topics <topics-testing-advanced-multidb>`. The :ref:`advanced multi-db testing topics <topics-testing-advanced-multidb>`.
.. _order-of-tests:
Order in which tests are executed Order in which tests are executed
--------------------------------- ---------------------------------
@ -908,8 +910,8 @@ TransactionTestCase
.. class:: TransactionTestCase() .. class:: TransactionTestCase()
Django ``TestCase`` classes make use of database transaction facilities, if Django's ``TestCase`` class (described below) makes use of database transaction
available, to speed up the process of resetting the database to a known state facilities to speed up the process of resetting the database to a known state
at the beginning of each test. A consequence of this, however, is that the at the beginning of each test. A consequence of this, however, is that the
effects of transaction commit and rollback cannot be tested by a Django effects of transaction commit and rollback cannot be tested by a Django
``TestCase`` class. If your test requires testing of such transactional ``TestCase`` class. If your test requires testing of such transactional
@ -927,9 +929,9 @@ to test the effects of commit and rollback:
Instead, it encloses the test code in a database transaction that is rolled Instead, it encloses the test code in a database transaction that is rolled
back at the end of the test. Both explicit commits like back at the end of the test. Both explicit commits like
``transaction.commit()`` and implicit ones that may be caused by ``transaction.commit()`` and implicit ones that may be caused by
``Model.save()`` are replaced with a ``nop`` operation. This guarantees that ``transaction.atomic()`` are replaced with a ``nop`` operation. This
the rollback at the end of the test restores the database to its initial guarantees that the rollback at the end of the test restores the database to
state. its initial state.
When running on a database that does not support rollback (e.g. MySQL with the When running on a database that does not support rollback (e.g. MySQL with the
MyISAM storage engine), ``TestCase`` falls back to initializing the database MyISAM storage engine), ``TestCase`` falls back to initializing the database
@ -940,22 +942,21 @@ to test the effects of commit and rollback:
While ``commit`` and ``rollback`` operations still *appear* to work when While ``commit`` and ``rollback`` operations still *appear* to work when
used in ``TestCase``, no actual commit or rollback will be performed by the used in ``TestCase``, no actual commit or rollback will be performed by the
database. This can cause your tests to pass or fail unexpectedly. Always database. This can cause your tests to pass or fail unexpectedly. Always
use ``TransactionalTestCase`` when testing transactional behavior. use ``TransactionTestCase`` when testing transactional behavior.
.. note::
.. versionchanged:: 1.5 .. versionchanged:: 1.5
Prior to 1.5, ``TransactionTestCase`` flushed the database tables *before* Prior to 1.5, :class:`~django.test.TransactionTestCase` flushed the
each test. In Django 1.5, this is instead done *after* the test has been run. database tables *before* each test. In Django 1.5, this is instead done
*after* the test has been run.
When the flush took place before the test, it was guaranteed that primary When the flush took place before the test, it was guaranteed that primary
key values started at one in :class:`~django.test.TransactionTestCase` key values started at one in :class:`~django.test.TransactionTestCase`
tests. tests.
Tests should not depend on this behavior, but for legacy tests that do, the Tests should not depend on this behavior, but for legacy tests that do,
:attr:`~TransactionTestCase.reset_sequences` attribute can be used until the :attr:`~TransactionTestCase.reset_sequences` attribute can be used
the test has been properly updated. until the test has been properly updated.
.. versionchanged:: 1.5 .. versionchanged:: 1.5
@ -964,55 +965,6 @@ to test the effects of commit and rollback:
``TransactionTestCase`` inherits from :class:`~django.test.SimpleTestCase`. ``TransactionTestCase`` inherits from :class:`~django.test.SimpleTestCase`.
.. attribute:: TransactionTestCase.reset_sequences
.. versionadded:: 1.5
Setting ``reset_sequences = True`` on a ``TransactionTestCase`` will make
sure sequences are always reset before the test run::
class TestsThatDependsOnPrimaryKeySequences(TransactionTestCase):
reset_sequences = True
def test_animal_pk(self):
lion = Animal.objects.create(name="lion", sound="roar")
# lion.pk is guaranteed to always be 1
self.assertEqual(lion.pk, 1)
Unless you are explicitly testing primary keys sequence numbers, it is
recommended that you do not hard code primary key values in tests.
Using ``reset_sequences = True`` will slow down the test, since the primary
key reset is an relatively expensive database operation.
.. attribute:: TransactionTestCase.available_apps
.. warning::
This attribute is a private API. It may be changed or removed without
a deprecation period in the future, for instance to accomodate changes
in application loading.
It's used to optimize Django's own test suite, which contains hundreds
of models but no relations between models in different applications.
.. versionadded:: 1.6
By default, ``available_apps`` is set to ``None`` and has no effect.
Setting it to a list of applications tells Django to behave as if only the
models from these applications were available:
- Before each test, Django creates content types and permissions only for
these models.
- After each test, Django flushes only the corresponding tables. However,
at the database level, truncation may cascade to other related models,
even if they aren't in ``available_apps``.
Since the database isn't fully flushed, if a test creates instances of
models not included in ``available_apps``, they will leak and they may
cause unrelated tests to fail. Be careful with tests that use sessions;
the default session engine stores them in the database.
TestCase TestCase
~~~~~~~~ ~~~~~~~~