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.
This commit is contained in:
Ramiro Morales 2012-07-24 17:24:16 -03:00
parent 38ce709fe4
commit f758bdab5e
11 changed files with 267 additions and 96 deletions

View File

@ -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.

View File

@ -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 File

@ -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

View File

@ -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:

View File

@ -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 = []

View File

@ -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)

View File

@ -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

View File

@ -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,
**{

View File

@ -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 File

@ -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.
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::
.. 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`.
.. 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.
.. 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 File

@ -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')