summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/core/management/commands/flush.py4
-rw-r--r--django/core/management/sql.py7
-rw-r--r--django/db/backends/__init__.py13
-rw-r--r--django/db/backends/mysql/base.py27
-rw-r--r--django/db/backends/oracle/base.py21
-rw-r--r--django/db/backends/postgresql_psycopg2/operations.py34
-rw-r--r--django/test/simple.py4
-rw-r--r--django/test/testcases.py62
-rw-r--r--docs/releases/1.5.txt51
-rw-r--r--docs/topics/testing.txt137
-rw-r--r--tests/regressiontests/test_runner/tests.py3
11 files changed, 267 insertions, 96 deletions
diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py
index 2fc2e7ed26..ac7b7a3599 100644
--- a/django/core/management/commands/flush.py
+++ b/django/core/management/commands/flush.py
@@ -29,6 +29,8 @@ class Command(NoArgsCommand):
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 @@ class Command(NoArgsCommand):
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.
diff --git a/django/core/management/sql.py b/django/core/management/sql.py
index 46d3cf28ed..7579cbe8ab 100644
--- a/django/core/management/sql.py
+++ b/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):
diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
index a896f5fd08..6e23ad5bb5 100644
--- a/django/db/backends/__init__.py
+++ b/django/db/backends/__init__.py
@@ -748,11 +748,24 @@ class BaseDatabaseOperations(object):
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
diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py
index ec65207ed8..2222f89cf0 100644
--- a/django/db/backends/mysql/base.py
+++ b/django/db/backends/mysql/base.py
@@ -262,22 +262,25 @@ class DatabaseOperations(BaseDatabaseOperations):
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:
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py
index 32ae420ce0..b08113fed7 100644
--- a/django/db/backends/oracle/base.py
+++ b/django/db/backends/oracle/base.py
@@ -298,18 +298,23 @@ WHEN (new.%(col_name)s IS NULL)
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 = []
diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py
index e93a15512b..40fe629110 100644
--- a/django/db/backends/postgresql_psycopg2/operations.py
+++ b/django/db/backends/postgresql_psycopg2/operations.py
@@ -85,25 +85,29 @@ class DatabaseOperations(BaseDatabaseOperations):
(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)
diff --git a/django/test/simple.py b/django/test/simple.py
index 4f05284543..bf0219d53f 100644
--- a/django/test/simple.py
+++ b/django/test/simple.py
@@ -5,7 +5,7 @@ from django.core.exceptions import ImproperlyConfigured
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 @@ class DjangoTestSuiteRunner(object):
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
diff --git a/django/test/testcases.py b/django/test/testcases.py
index b9aae21e8e..b60188bf30 100644
--- a/django/test/testcases.py
+++ b/django/test/testcases.py
@@ -23,6 +23,7 @@ from django.core import mail
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 @@ class SimpleTestCase(ut2.TestCase):
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 @@ class TransactionTestCase(SimpleTestCase):
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 @@ class TransactionTestCase(SimpleTestCase):
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 @@ class TestCase(TransactionTestCase):
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,
**{
diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt
index fd9ae4f038..aae8b25e07 100644
--- a/docs/releases/1.5.txt
+++ b/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
~~~~~~~~~~~~~
diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt
index aa274d83c9..1f4c970d3e 100644
--- a/docs/topics/testing.txt
+++ b/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.
+to test the effects of commit and rollback:
-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.
+* 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.
-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.
+* 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.
-``TransactionTestCase`` inherits from :class:`~django.test.SimpleTestCase`.
+ 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()
diff --git a/tests/regressiontests/test_runner/tests.py b/tests/regressiontests/test_runner/tests.py
index 8c6dabf771..c723f162a4 100644
--- a/tests/regressiontests/test_runner/tests.py
+++ b/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')