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. + + 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() 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')