From 55cbd65985bfad02512a64a4cb8468140f15ee84 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 11 Jun 2013 22:56:09 +0200 Subject: [PATCH] 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. --- django/core/management/commands/flush.py | 28 +++++---- django/test/testcases.py | 35 +++++++---- docs/topics/testing/advanced.txt | 74 ++++++++++++++++++++++ docs/topics/testing/overview.txt | 80 +++++------------------- 4 files changed, 130 insertions(+), 87 deletions(-) diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py index 7054187e45..95dd634d08 100644 --- a/django/core/management/commands/flush.py +++ b/django/core/management/commands/flush.py @@ -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) diff --git a/django/test/testcases.py b/django/test/testcases.py index 1dbb82e352..984d21ce18 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -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) diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt index b7f49d2b97..8f7f093901 100644 --- a/docs/topics/testing/advanced.txt +++ b/docs/topics/testing/advanced.txt @@ -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 `, 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 ===================================== diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index 6f73f7bbb5..8268051a36 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -213,6 +213,8 @@ advanced settings. The :ref:`advanced multi-db testing topics `. +.. _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.