Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Fixed #18271 -- Changed stage at which TransactionTestCase flushes DB…

… tables.

Previously, the flush was done before the test case execution and now
it is performed after it.

Other changes to the testing infrastructure include:

* TransactionTestCase now doesn't reset autoincrement sequences either
  (previous behavior can achieved by using `reset_sequences`.)
  With this, no implicit such reset is performed by any of the provided
  TestCase classes.

* New ordering of test cases: All unittest tes cases are run first and
  doctests are run at the end.

THse changes could be backward-incompatible with test cases that relied
on some kind of state being preserved between tests. Please read the
relevant sections of the release notes and testing documentation for
further details.

Thanks Andreas Pelme for the initial patch. Karen Tracey and Anssi
Kääriäinen for the feedback and Anssi for reviewing.

This also fixes #12408.
  • Loading branch information...
commit f758bdab5eec3e615598948dd5bcf9bb7b910c9d 1 parent 38ce709
@ramiro ramiro authored
View
4 django/core/management/commands/flush.py
@@ -29,6 +29,8 @@ def handle_noargs(self, **options):
connection = connections[db]
verbosity = int(options.get('verbosity'))
interactive = options.get('interactive')
+ # 'reset_sequences' is a stealth option
+ reset_sequences = options.get('reset_sequences', True)
self.style = no_style()
@@ -40,7 +42,7 @@ def handle_noargs(self, **options):
except ImportError:
pass
- sql_list = sql_flush(self.style, connection, only_django=True)
+ sql_list = sql_flush(self.style, connection, only_django=True, reset_sequences=reset_sequences)
if interactive:
confirm = raw_input("""You have requested a flush of the database.
View
7 django/core/management/sql.py
@@ -98,7 +98,7 @@ def sql_delete(app, style, connection):
return output[::-1] # Reverse it, to deal with table dependencies.
-def sql_flush(style, connection, only_django=False):
+def sql_flush(style, connection, only_django=False, reset_sequences=True):
"""
Returns a list of the SQL statements used to flush the database.
@@ -109,9 +109,8 @@ def sql_flush(style, connection, only_django=False):
tables = connection.introspection.django_table_names(only_existing=True)
else:
tables = connection.introspection.table_names()
- statements = connection.ops.sql_flush(
- style, tables, connection.introspection.sequence_list()
- )
+ seqs = connection.introspection.sequence_list() if reset_sequences else ()
+ statements = connection.ops.sql_flush(style, tables, seqs)
return statements
def sql_custom(app, style, connection):
View
13 django/db/backends/__init__.py
@@ -748,11 +748,24 @@ def sql_flush(self, style, tables, sequences):
the given database tables (without actually removing the tables
themselves).
+ The returned value also includes SQL statements required to reset DB
+ sequences passed in :param sequences:.
+
The `style` argument is a Style object as returned by either
color_style() or no_style() in django.core.management.color.
"""
raise NotImplementedError()
+ def sequence_reset_by_name_sql(self, style, sequences):
+ """
+ Returns a list of the SQL statements required to reset sequences
+ passed in :param sequences:.
+
+ The `style` argument is a Style object as returned by either
+ color_style() or no_style() in django.core.management.color.
+ """
+ return []
+
def sequence_reset_sql(self, style, model_list):
"""
Returns a list of the SQL statements required to reset sequences for
View
27 django/db/backends/mysql/base.py
@@ -262,22 +262,25 @@ def sql_flush(self, style, tables, sequences):
for table in tables:
sql.append('%s %s;' % (style.SQL_KEYWORD('TRUNCATE'), style.SQL_FIELD(self.quote_name(table))))
sql.append('SET FOREIGN_KEY_CHECKS = 1;')
-
- # Truncate already resets the AUTO_INCREMENT field from
- # MySQL version 5.0.13 onwards. Refs #16961.
- if self.connection.mysql_version < (5,0,13):
- sql.extend(
- ["%s %s %s %s %s;" % \
- (style.SQL_KEYWORD('ALTER'),
- style.SQL_KEYWORD('TABLE'),
- style.SQL_TABLE(self.quote_name(sequence['table'])),
- style.SQL_KEYWORD('AUTO_INCREMENT'),
- style.SQL_FIELD('= 1'),
- ) for sequence in sequences])
+ sql.extend(self.sequence_reset_by_name_sql(style, sequences))
return sql
else:
return []
+ def sequence_reset_by_name_sql(self, style, sequences):
+ # Truncate already resets the AUTO_INCREMENT field from
+ # MySQL version 5.0.13 onwards. Refs #16961.
+ if self.connection.mysql_version < (5, 0, 13):
+ return ["%s %s %s %s %s;" % \
+ (style.SQL_KEYWORD('ALTER'),
+ style.SQL_KEYWORD('TABLE'),
+ style.SQL_TABLE(self.quote_name(sequence['table'])),
+ style.SQL_KEYWORD('AUTO_INCREMENT'),
+ style.SQL_FIELD('= 1'),
+ ) for sequence in sequences]
+ else:
+ return []
+
def validate_autopk_value(self, value):
# MySQLism: zero in AUTO_INCREMENT field does not work. Refs #17653.
if value == 0:
View
21 django/db/backends/oracle/base.py
@@ -298,18 +298,23 @@ def sql_flush(self, style, tables, sequences):
for table in tables]
# Since we've just deleted all the rows, running our sequence
# ALTER code will reset the sequence to 0.
- for sequence_info in sequences:
- sequence_name = self._get_sequence_name(sequence_info['table'])
- table_name = self.quote_name(sequence_info['table'])
- column_name = self.quote_name(sequence_info['column'] or 'id')
- query = _get_sequence_reset_sql() % {'sequence': sequence_name,
- 'table': table_name,
- 'column': column_name}
- sql.append(query)
+ sql.extend(self.sequence_reset_by_name_sql(style, sequences))
return sql
else:
return []
+ def sequence_reset_by_name_sql(self, style, sequences):
+ sql = []
+ for sequence_info in sequences:
+ sequence_name = self._get_sequence_name(sequence_info['table'])
+ table_name = self.quote_name(sequence_info['table'])
+ column_name = self.quote_name(sequence_info['column'] or 'id')
+ query = _get_sequence_reset_sql() % {'sequence': sequence_name,
+ 'table': table_name,
+ 'column': column_name}
+ sql.append(query)
+ return sql
+
def sequence_reset_sql(self, style, model_list):
from django.db import models
output = []
View
34 django/db/backends/postgresql_psycopg2/operations.py
@@ -85,25 +85,29 @@ def sql_flush(self, style, tables, sequences):
(style.SQL_KEYWORD('TRUNCATE'),
style.SQL_FIELD(', '.join([self.quote_name(table) for table in tables]))
)]
-
- # 'ALTER SEQUENCE sequence_name RESTART WITH 1;'... style SQL statements
- # to reset sequence indices
- for sequence_info in sequences:
- table_name = sequence_info['table']
- column_name = sequence_info['column']
- if not (column_name and len(column_name) > 0):
- # This will be the case if it's an m2m using an autogenerated
- # intermediate table (see BaseDatabaseIntrospection.sequence_list)
- column_name = 'id'
- sql.append("%s setval(pg_get_serial_sequence('%s','%s'), 1, false);" % \
- (style.SQL_KEYWORD('SELECT'),
- style.SQL_TABLE(self.quote_name(table_name)),
- style.SQL_FIELD(column_name))
- )
+ sql.extend(self.sequence_reset_by_name_sql(style, sequences))
return sql
else:
return []
+ def sequence_reset_by_name_sql(self, style, sequences):
+ # 'ALTER SEQUENCE sequence_name RESTART WITH 1;'... style SQL statements
+ # to reset sequence indices
+ sql = []
+ for sequence_info in sequences:
+ table_name = sequence_info['table']
+ column_name = sequence_info['column']
+ if not (column_name and len(column_name) > 0):
+ # This will be the case if it's an m2m using an autogenerated
+ # intermediate table (see BaseDatabaseIntrospection.sequence_list)
+ column_name = 'id'
+ sql.append("%s setval(pg_get_serial_sequence('%s','%s'), 1, false);" % \
+ (style.SQL_KEYWORD('SELECT'),
+ style.SQL_TABLE(self.quote_name(table_name)),
+ style.SQL_FIELD(column_name))
+ )
+ return sql
+
def tablespace_sql(self, tablespace, inline=False):
if inline:
return "USING INDEX TABLESPACE %s" % self.quote_name(tablespace)
View
4 django/test/simple.py
@@ -5,7 +5,7 @@
from django.db.models import get_app, get_apps
from django.test import _doctest as doctest
from django.test.utils import setup_test_environment, teardown_test_environment
-from django.test.testcases import OutputChecker, DocTestRunner, TestCase
+from django.test.testcases import OutputChecker, DocTestRunner
from django.utils import unittest
from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule
@@ -263,7 +263,7 @@ def build_suite(self, test_labels, extra_tests=None, **kwargs):
for test in extra_tests:
suite.addTest(test)
- return reorder_suite(suite, (TestCase,))
+ return reorder_suite(suite, (unittest.TestCase,))
def setup_databases(self, **kwargs):
from django.db import connections, DEFAULT_DB_ALIAS
View
62 django/test/testcases.py
@@ -23,6 +23,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.signals import request_started
from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer,
WSGIServerException)
@@ -444,10 +445,15 @@ def assertHTMLNotEqual(self, html1, html2, msg=None):
class TransactionTestCase(SimpleTestCase):
+
# The class we'll use for the test client self.client.
# Can be overridden in derived classes.
client_class = Client
+ # Subclasses can ask for resetting of auto increment sequence before each
+ # test case
+ reset_sequences = False
+
def _pre_setup(self):
"""Performs any pre-test setup. This includes:
@@ -462,22 +468,36 @@ def _pre_setup(self):
self._urlconf_setup()
mail.outbox = []
+ def _reset_sequences(self, db_name):
+ conn = connections[db_name]
+ if conn.features.supports_sequence_reset:
+ sql_list = \
+ conn.ops.sequence_reset_by_name_sql(no_style(),
+ conn.introspection.sequence_list())
+ if sql_list:
+ try:
+ cursor = conn.cursor()
+ for sql in sql_list:
+ cursor.execute(sql)
+ except Exception:
+ transaction.rollback_unless_managed(using=db_name)
+ raise
+ transaction.commit_unless_managed(using=db_name)
+
def _fixture_setup(self):
- # If the test case has a multi_db=True flag, flush all databases.
- # Otherwise, just flush default.
- if getattr(self, 'multi_db', False):
- databases = connections
- else:
- databases = [DEFAULT_DB_ALIAS]
- for db in databases:
- call_command('flush', verbosity=0, interactive=False, database=db,
- skip_validation=True)
+ # If the test case has a multi_db=True flag, act on all databases.
+ # Otherwise, just on the default DB.
+ db_names = connections if getattr(self, 'multi_db', False) else [DEFAULT_DB_ALIAS]
+ for db_name in db_names:
+ # Reset sequences
+ if self.reset_sequences:
+ self._reset_sequences(db_name)
if hasattr(self, 'fixtures'):
# We have to use this slightly awkward syntax due to the fact
# that we're using *args and **kwargs together.
call_command('loaddata', *self.fixtures,
- **{'verbosity': 0, 'database': db, 'skip_validation': True})
+ **{'verbosity': 0, 'database': db_name, 'skip_validation': True})
def _urlconf_setup(self):
if hasattr(self, 'urls'):
@@ -534,7 +554,12 @@ def _post_teardown(self):
conn.close()
def _fixture_teardown(self):
- pass
+ # If the test case has a multi_db=True flag, flush all databases.
+ # Otherwise, just flush default.
+ databases = connections if getattr(self, 'multi_db', False) else [DEFAULT_DB_ALIAS]
+ for db in databases:
+ call_command('flush', verbosity=0, interactive=False, database=db,
+ skip_validation=True, reset_sequences=False)
def _urlconf_teardown(self):
if hasattr(self, '_old_root_urlconf'):
@@ -808,22 +833,21 @@ def _fixture_setup(self):
if not connections_support_transactions():
return super(TestCase, self)._fixture_setup()
+ assert not self.reset_sequences, 'reset_sequences cannot be used on TestCase instances'
+
# If the test case has a multi_db=True flag, setup all databases.
# Otherwise, just use default.
- if getattr(self, 'multi_db', False):
- databases = connections
- else:
- databases = [DEFAULT_DB_ALIAS]
+ db_names = connections if getattr(self, 'multi_db', False) else [DEFAULT_DB_ALIAS]
- for db in databases:
- transaction.enter_transaction_management(using=db)
- transaction.managed(True, using=db)
+ for db_name in db_names:
+ transaction.enter_transaction_management(using=db_name)
+ transaction.managed(True, using=db_name)
disable_transaction_methods()
from django.contrib.sites.models import Site
Site.objects.clear_cache()
- for db in databases:
+ for db in db_names:
if hasattr(self, 'fixtures'):
call_command('loaddata', *self.fixtures,
**{
View
51 docs/releases/1.5.txt
@@ -188,6 +188,57 @@ Session not saved on 500 responses
Django's session middleware will skip saving the session data if the
response's status code is 500.
+Changes in tests execution
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Some changes have been introduced in the execution of tests that might be
+backward-incompatible for some testing setups:
+
+Database flushing in ``django.test.TransactionTestCase``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Previously, the test database was truncated *before* each test run in a
+:class:`~django.test.TransactionTestCase`.
+
+In order to be able to run unit tests in any order and to make sure they are
+always isolated from each other, :class:`~django.test.TransactionTestCase` will
+now reset the database *after* each test run instead.
+
+No more implict DB sequences reset
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:class:`~django.test.TransactionTestCase` tests used to reset primary key
+sequences automatically together with the database flushing actions described
+above.
+
+This has been changed so no sequences are implicitly reset. This can cause
+:class:`~django.test.TransactionTestCase` tests that depend on hard-coded
+primary key values to break.
+
+The new :attr:`~django.test.TransactionTestCase.reset_sequences` attribute can
+be used to force the old behavior for :class:`~django.test.TransactionTestCase`
+that might need it.
+
+Ordering of tests
+^^^^^^^^^^^^^^^^^
+
+In order to make sure all ``TestCase`` code starts with a clean database,
+tests are now executed in the following order:
+
+* First, all unittests (including :class:`unittest.TestCase`,
+ :class:`~django.test.SimpleTestCase`, :class:`~django.test.TestCase` and
+ :class:`~django.test.TransactionTestCase`) are run with no particular ordering
+ guaranteed nor enforced among them.
+
+* Then any other tests (e.g. doctests) that may alter the database without
+ restoring it to its original state are run.
+
+This should not cause any problems unless you have existing doctests which
+assume a :class:`~django.test.TransactionTestCase` executed earlier left some
+database state behind or unit tests that rely on some form of state being
+preserved after the execution of other tests. Such tests are already very
+fragile, and must now be changed to be able to run independently.
+
Miscellaneous
~~~~~~~~~~~~~
View
141 docs/topics/testing.txt
@@ -478,6 +478,32 @@ If there are any circular dependencies in the
:setting:`TEST_DEPENDENCIES` definition, an ``ImproperlyConfigured``
exception will be raised.
+Order in which tests are executed
+---------------------------------
+
+In order to guarantee that all ``TestCase`` code starts with a clean database,
+the Django test runner reorders tests in the following way:
+
+* First, all unittests (including :class:`unittest.TestCase`,
+ :class:`~django.test.SimpleTestCase`, :class:`~django.test.TestCase` and
+ :class:`~django.test.TransactionTestCase`) are run with no particular ordering
+ guaranteed nor enforced among them.
+
+* Then any other tests (e.g. doctests) that may alter the database without
+ restoring it to its original state are run.
+
+.. versionchanged:: 1.5
+ Before Django 1.5, the only guarantee was that
+ :class:`~django.test.TestCase` tests were always ran first, before any other
+ tests.
+
+.. note::
+
+ The new ordering of tests may reveal unexpected dependencies on test case
+ ordering. This is the case with doctests that relied on state left in the
+ database by a given :class:`~django.test.TransactionTestCase` test, they
+ must be updated to be able to run independently.
+
Other test conditions
---------------------
@@ -1109,8 +1135,11 @@ The following is a simple unit test using the request factory::
response = my_view(request)
self.assertEqual(response.status_code, 200)
-TestCase
---------
+Test cases
+----------
+
+Provided test case classes
+~~~~~~~~~~~~~~~~~~~~~~~~~~
.. currentmodule:: django.test
@@ -1124,16 +1153,19 @@ Normal Python unit test classes extend a base class of
Hierarchy of Django unit testing classes
+TestCase
+^^^^^^^^
+
.. class:: TestCase()
This class provides some additional capabilities that can be useful for testing
Web sites.
Converting a normal :class:`unittest.TestCase` to a Django :class:`TestCase` is
-easy: just change the base class of your test from :class:`unittest.TestCase` to
-:class:`django.test.TestCase`. All of the standard Python unit test
-functionality will continue to be available, but it will be augmented with some
-useful additions, including:
+easy: Just change the base class of your test from `'unittest.TestCase'` to
+`'django.test.TestCase'`. All of the standard Python unit test functionality
+will continue to be available, but it will be augmented with some useful
+additions, including:
* Automatic loading of fixtures.
@@ -1141,11 +1173,18 @@ useful additions, including:
* Creates a TestClient instance.
-* Django-specific assertions for testing for things
- like redirection and form errors.
+* Django-specific assertions for testing for things like redirection and form
+ errors.
+
+.. versionchanged:: 1.5
+ The order in which tests are run has changed. See `Order in which tests are
+ executed`_.
``TestCase`` inherits from :class:`~django.test.TransactionTestCase`.
+TransactionTestCase
+^^^^^^^^^^^^^^^^^^^
+
.. class:: TransactionTestCase()
Django ``TestCase`` classes make use of database transaction facilities, if
@@ -1157,38 +1196,66 @@ behavior, you should use a Django ``TransactionTestCase``.
``TransactionTestCase`` and ``TestCase`` are identical except for the manner
in which the database is reset to a known state and the ability for test code
-to test the effects of commit and rollback. A ``TransactionTestCase`` resets
-the database before the test runs by truncating all tables and reloading
-initial data. A ``TransactionTestCase`` may call commit and rollback and
-observe the effects of these calls on the database.
-
-A ``TestCase``, on the other hand, does not truncate tables and reload initial
-data at the beginning of a test. Instead, it encloses the test code in a
-database transaction that is rolled back at the end of the test. It also
-prevents the code under test from issuing any commit or rollback operations
-on the database, to ensure that the rollback at the end of the test restores
-the database to its initial state. In order to guarantee that all ``TestCase``
-code starts with a clean database, the Django test runner runs all ``TestCase``
-tests first, before any other tests (e.g. doctests) that may alter the
-database without restoring it to its original 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
-by truncating tables and reloading initial data.
+to test the effects of commit and rollback:
-``TransactionTestCase`` inherits from :class:`~django.test.SimpleTestCase`.
+* A ``TransactionTestCase`` resets the database after the test runs by
+ truncating all tables. A ``TransactionTestCase`` may call commit and rollback
+ and observe the effects of these calls on the database.
+
+* A ``TestCase``, on the other hand, does not truncate tables after a test.
+ Instead, it encloses the test code in a database transaction that is rolled
+ back at the end of the test. It also prevents the code under test from
+ issuing any commit or rollback operations on the database, to ensure 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
+ by truncating tables and reloading initial data.
.. note::
- The ``TestCase`` use of rollback to un-do the effects of the test code
- may reveal previously-undetected errors in test code. For example,
- test code that assumes primary keys values will be assigned starting at
- one may find that assumption no longer holds true when rollbacks instead
- of table truncation are being used to reset the database. Similarly,
- the reordering of tests so that all ``TestCase`` classes run first may
- reveal unexpected dependencies on test case ordering. In such cases a
- quick fix is to switch the ``TestCase`` to a ``TransactionTestCase``.
- A better long-term fix, that allows the test to take advantage of the
- speed benefit of ``TestCase``, is to fix the underlying test problem.
+
+ .. 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.
+
+ 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 behaviour, 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
+ The order in which tests are run has changed. See `Order in which tests are
+ executed`_.
+
+``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.
+
+SimpleTestCase
+^^^^^^^^^^^^^^
.. class:: SimpleTestCase()
View
3  tests/regressiontests/test_runner/tests.py
@@ -267,6 +267,9 @@ class AutoIncrementResetTest(TransactionTestCase):
and check that both times they get "1" as their PK value. That is, we test
that AutoField values start from 1 for each transactional test case.
"""
+
+ reset_sequences = True
+
@skipUnlessDBFeature('supports_sequence_reset')
def test_autoincrement_reset1(self):
p = Person.objects.create(first_name='Jack', last_name='Smith')
Please sign in to comment.
Something went wrong with that request. Please try again.