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]
verbosity = int(options.get('verbosity'))
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)
allow_cascade = options.get('allow_cascade', False)
inhibit_post_syncdb = options.get('inhibit_post_syncdb', False)
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"
"The full error: %s") % (connection.settings_dict['NAME'], e)
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
# 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(db, m)
])
emit_post_sync_signal(set(all_models), verbosity, interactive, db)
if not inhibit_post_syncdb:
self.emit_post_syncdb(verbosity, interactive, db)
# Reinstall the initial_data fixture.
if options.get('load_initial_data'):
@ -93,3 +87,15 @@ Are you sure you want to do this?
else:
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.management import call_command
from django.core.management.color import no_style
from django.core.management.commands import flush
from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer,
WSGIServerException)
from django.core.urlresolvers import clear_url_caches, set_urlconf
@ -196,9 +197,9 @@ class SimpleTestCase(ut2.TestCase):
def _pre_setup(self):
"""Performs any pre-test setup. This includes:
* If the Test Case class has a 'urls' member, replace the
ROOT_URLCONF with it.
* Clearing the mail test outbox.
* Creating a test client.
* If the class has a 'urls' attribute, replace ROOT_URLCONF with it.
* Clearing the mail test outbox.
"""
self.client = self.client_class()
self._urlconf_setup()
@ -212,6 +213,10 @@ class SimpleTestCase(ut2.TestCase):
clear_url_caches()
def _post_teardown(self):
"""Performs any post-test things. This includes:
* Putting back the original ROOT_URLCONF if it was changed.
"""
self._urlconf_teardown()
def _urlconf_teardown(self):
@ -732,13 +737,17 @@ class TransactionTestCase(SimpleTestCase):
def _pre_setup(self):
"""Performs any pre-test setup. This includes:
* Flushing the database.
* If the Test Case class has a 'fixtures' member, installing the
named fixtures.
* If the class has an 'available_apps' attribute, restricting the app
cache to these applications, then firing post_syncdb -- it must run
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()
if self.available_apps is not None:
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:
self._fixture_setup()
except Exception:
@ -782,9 +791,9 @@ class TransactionTestCase(SimpleTestCase):
def _post_teardown(self):
"""Performs any post-test things. This includes:
* Putting back the original ROOT_URLCONF if it was changed.
* Force closing the connection, so that the next test gets
a clean cursor.
* Flushing the contents of the database, to leave a clean slate. If
the class has an 'available_apps' attribute, post_syncdb isn't fired.
* Force-closing the connection, so the next test gets a clean cursor.
"""
try:
self._fixture_teardown()
@ -801,12 +810,14 @@ class TransactionTestCase(SimpleTestCase):
cache.unset_available_apps()
def _fixture_teardown(self):
# Allow TRUNCATE ... CASCADE when flushing only a subset of the apps
allow_cascade = self.available_apps is not None
# Allow TRUNCATE ... CASCADE and don't emit the post_syncdb signal
# when flushing only a subset of the apps
for db_name in self._databases_names(include_mirrors=False):
call_command('flush', verbosity=0, interactive=False,
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):
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``
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
=====================================

View File

@ -213,6 +213,8 @@ advanced settings.
The :ref:`advanced multi-db testing topics <topics-testing-advanced-multidb>`.
.. _order-of-tests:
Order in which tests are executed
---------------------------------
@ -908,8 +910,8 @@ TransactionTestCase
.. class:: TransactionTestCase()
Django ``TestCase`` classes make use of database transaction facilities, if
available, to speed up the process of resetting the database to a known state
Django's ``TestCase`` class (described below) makes use of database transaction
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
effects of transaction commit and rollback cannot be tested by a Django
``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
back at the end of the test. Both explicit commits like
``transaction.commit()`` and implicit ones that may be caused by
``Model.save()`` are replaced with a ``nop`` operation. This guarantees that
the rollback at the end of the test restores the database to its initial
state.
``transaction.atomic()`` are replaced with a ``nop`` operation. This
guarantees that the rollback at the end of the test restores the database to
its initial state.
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
@ -940,22 +942,21 @@ to test the effects of commit and rollback:
While ``commit`` and ``rollback`` operations still *appear* to work when
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
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*
each test. In Django 1.5, this is instead done *after* the test has been run.
Prior to 1.5, :class:`~django.test.TransactionTestCase` flushed the
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
key values started at one in :class:`~django.test.TransactionTestCase`
tests.
Tests should not depend on this behavior, but for legacy tests that do, the
:attr:`~TransactionTestCase.reset_sequences` attribute can be used until
the test has been properly updated.
Tests should not depend on this behavior, but for legacy tests that do,
the :attr:`~TransactionTestCase.reset_sequences` attribute can be used
until the test has been properly updated.
.. versionchanged:: 1.5
@ -964,55 +965,6 @@ to test the effects of commit and rollback:
``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
~~~~~~~~
@ -1495,7 +1447,7 @@ Emptying the test outbox
~~~~~~~~~~~~~~~~~~~~~~~~
If you use any of Django's custom ``TestCase`` classes, the test runner will
clear thecontents of the test email outbox at the start of each test case.
clear the contents of the test email outbox at the start of each test case.
For more detail on email services during tests, see `Email services`_ below.