Fixed #12624 -- Modified test runners to be class based.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@12255 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2010-01-18 15:11:01 +00:00
parent 34e420184c
commit 53b61d9c02
7 changed files with 216 additions and 82 deletions

View File

@ -487,8 +487,8 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.user_messages.LegacyFallbackS
# TESTING # # TESTING #
########### ###########
# The name of the method to use to invoke the test suite # The name of the class to use to run the test suite
TEST_RUNNER = 'django.test.simple.run_tests' TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner'
# The name of the database to use for testing purposes. # The name of the database to use for testing purposes.
# If None, a name of 'test_' + DATABASE_NAME will be assumed # If None, a name of 'test_' + DATABASE_NAME will be assumed

View File

@ -21,14 +21,20 @@ class Command(BaseCommand):
verbosity = int(options.get('verbosity', 1)) verbosity = int(options.get('verbosity', 1))
interactive = options.get('interactive', True) interactive = options.get('interactive', True)
failfast = options.get('failfast', False) failfast = options.get('failfast', False)
test_runner = get_runner(settings) TestRunner = get_runner(settings)
# Some custom test runners won't accept the failfast flag, so let's make sure they accept it before passing it to them if hasattr(TestRunner, 'func_name'):
if 'failfast' in test_runner.func_code.co_varnames: # Pre 1.2 test runners were just functions,
failures = test_runner(test_labels, verbosity=verbosity, interactive=interactive, # and did not support the 'failfast' option.
failfast=failfast) import warnings
warnings.warn(
'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.',
PendingDeprecationWarning
)
failures = TestRunner(test_labels, verbosity=verbosity, interactive=interactive)
else: else:
failures = test_runner(test_labels, verbosity=verbosity, interactive=interactive) test_runner = TestRunner(verbosity=verbosity, interactive=interactive, failfast=failfast)
failures = test_runner.run_tests(test_labels)
if failures: if failures:
sys.exit(bool(failures)) sys.exit(bool(failures))

View File

@ -197,7 +197,60 @@ def reorder_suite(suite, classes):
bins[0].addTests(bins[i+1]) bins[0].addTests(bins[i+1])
return bins[0] return bins[0]
def run_tests(test_labels, verbosity=1, interactive=True, failfast=False, extra_tests=[]):
class DjangoTestSuiteRunner(object):
def __init__(self, verbosity=1, interactive=True, failfast=True):
self.verbosity = verbosity
self.interactive = interactive
self.failfast = failfast
def setup_test_environment(self):
setup_test_environment()
settings.DEBUG = False
def build_suite(self, test_labels, extra_tests=None):
suite = unittest.TestSuite()
if test_labels:
for label in test_labels:
if '.' in label:
suite.addTest(build_test(label))
else:
app = get_app(label)
suite.addTest(build_suite(app))
else:
for app in get_apps():
suite.addTest(build_suite(app))
if extra_tests:
for test in extra_tests:
suite.addTest(test)
return reorder_suite(suite, (TestCase,))
def setup_databases(self):
from django.db import connections
old_names = []
for alias in connections:
connection = connections[alias]
old_names.append((connection, connection.settings_dict['NAME']))
connection.creation.create_test_db(self.verbosity, autoclobber=not self.interactive)
return old_names
def run_suite(self, suite):
return DjangoTestRunner(verbosity=self.verbosity, failfast=self.failfast).run(suite)
def teardown_databases(self, old_names):
for connection, old_name in old_names:
connection.creation.destroy_test_db(old_name, self.verbosity)
def teardown_test_environment(self):
teardown_test_environment()
def suite_result(self, result):
return len(result.failures) + len(result.errors)
def run_tests(self, test_labels, extra_tests=None):
""" """
Run the unit tests for all the test labels in the provided list. Run the unit tests for all the test labels in the provided list.
Labels must be of the form: Labels must be of the form:
@ -216,37 +269,25 @@ def run_tests(test_labels, verbosity=1, interactive=True, failfast=False, extra_
Returns the number of tests that failed. Returns the number of tests that failed.
""" """
setup_test_environment() self.setup_test_environment()
settings.DEBUG = False old_names = self.setup_databases()
suite = unittest.TestSuite()
if test_labels: suite = self.build_suite(test_labels, extra_tests)
for label in test_labels:
if '.' in label:
suite.addTest(build_test(label))
else:
app = get_app(label)
suite.addTest(build_suite(app))
else:
for app in get_apps():
suite.addTest(build_suite(app))
for test in extra_tests: result = self.run_suite(suite)
suite.addTest(test)
suite = reorder_suite(suite, (TestCase,)) self.teardown_databases(old_names)
from django.db import connections self.teardown_test_environment()
old_names = []
for alias in connections:
connection = connections[alias]
old_names.append((connection, connection.settings_dict['NAME']))
connection.creation.create_test_db(verbosity, autoclobber=not interactive)
result = DjangoTestRunner(verbosity=verbosity, failfast=failfast).run(suite)
for connection, old_name in old_names:
connection.creation.destroy_test_db(old_name, verbosity)
teardown_test_environment() return self.suite_result(result)
return len(result.failures) + len(result.errors) def run_tests(test_labels, verbosity=1, interactive=True, failfast=False, extra_tests=None):
import warnings
warnings.warn(
'The run_tests() test runner has been deprecated in favor of DjangoTestSuiteRunner.',
PendingDeprecationWarning
)
test_runner = DjangoTestSuiteRunner(verbosity=verbosity, interactive=interactive, failfast=failfast)
return test_runner.run_tests(test_labels, extra_tests=extra_tests)

View File

@ -74,6 +74,9 @@ their deprecation, as per the :ref:`Django deprecation policy
``django.utils.formats.get_format()`` to get the appropriate ``django.utils.formats.get_format()`` to get the appropriate
formats. formats.
* The ability to use a function-based test runners will be removed,
along with the ``django.test.simple.run_tests()`` test runner.
* 2.0 * 2.0
* ``django.views.defaults.shortcut()``. This function has been moved * ``django.views.defaults.shortcut()``. This function has been moved
to ``django.contrib.contenttypes.views.shortcut()`` as part of the to ``django.contrib.contenttypes.views.shortcut()`` as part of the

View File

@ -367,6 +367,14 @@ An undocumented regex for validating email addresses has been moved from
django.form.fields to django.core.validators. You will need to update django.form.fields to django.core.validators. You will need to update
your imports if you are using it. your imports if you are using it.
Function-based test runners
---------------------------
Django 1.2 changes the test runner tools to use a class-based
approach. Old style function-based test runners will still work, but
should be updated to use the new :ref:`class-based runners
<topics-testing-test_runner>`.
What's new in Django 1.2 What's new in Django 1.2
======================== ========================

View File

@ -1228,41 +1228,65 @@ alternative framework as if they were normal Django tests.
When you run ``./manage.py test``, Django looks at the :setting:`TEST_RUNNER` When you run ``./manage.py test``, Django looks at the :setting:`TEST_RUNNER`
setting to determine what to do. By default, :setting:`TEST_RUNNER` points to setting to determine what to do. By default, :setting:`TEST_RUNNER` points to
``'django.test.simple.run_tests'``. This method defines the default Django ``'django.test.simple.DjangoTestSuiteRunner'``. This class defines the default Django
testing behavior. This behavior involves: testing behavior. This behavior involves:
#. Performing global pre-test setup. #. Performing global pre-test setup.
#. Creating the test database. #. Creating the test databases.
#. Running ``syncdb`` to install models and initial data into the test #. Running ``syncdb`` to install models and initial data into the test
database. databases.
#. Looking for unit tests and doctests in the ``models.py`` and #. Looking for unit tests and doctests in the ``models.py`` and
``tests.py`` files in each installed application. ``tests.py`` files in each installed application.
#. Running the unit tests and doctests that are found. #. Running the unit tests and doctests that are found.
#. Destroying the test database. #. Destroying the test databases.
#. Performing global post-test teardown. #. Performing global post-test teardown.
If you define your own test runner method and point :setting:`TEST_RUNNER` at If you define your own test runner method and point :setting:`TEST_RUNNER` at
that method, Django will execute your test runner whenever you run that method, Django will execute your test runner whenever you run
``./manage.py test``. In this way, it is possible to use any test framework ``./manage.py test``. In this way, it is possible to use any test framework
that can be executed from Python code. that can be executed from Python code, or to modify the Django test execution
process to satisfy whatever testing requirements you may have.
.. _topics-testing-test_runner:
Defining a test runner Defining a test runner
---------------------- ----------------------
.. versionadded:: 1.0 .. versionchanged:: 1.2
Prior to 1.2, test runners were a single function, not a class.
.. currentmodule:: django.test.simple .. currentmodule:: django.test.simple
By convention, a test runner should be called ``run_tests``. The only strict A test runner is a class defining a ``run_tests()`` method. Django ships
requirement is that it has the same arguments as the Django test runner: with a ``DjangoTestSuiteRunner`` class that defines the default Django
testing behavior. This class defines the ``run_tests()`` entry point,
plus a selection of other methods that are used to by ``run_tests()`` to
set up, execute and tear down the test suite.
.. function:: run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[]) .. class:: DjangoTestSuiteRunner(verbosity=1, interactive=True, failfast=True)
``verbosity`` determines the amount of notification and debug information
that will be printed to the console; ``0`` is no output, ``1`` is normal
output, and ``2`` is verbose output.
If ``interactive`` is ``True``, the test suite has permission to ask the
user for instructions when the test suite is executed. An example of this
behavior would be asking for permission to delete an existing test
database. If ``interactive`` is ``False``, the test suite must be able to
run without any manual intervention.
If ``failfast`` is ``True``, the test suite will stop running after the
first test failure is detected.
.. method:: DjangoTestSuiteRunner.run_tests(test_labels, extra_tests=[])
Run the test suite.
``test_labels`` is a list of strings describing the tests to be run. A test ``test_labels`` is a list of strings describing the tests to be run. A test
label can take one of three forms: label can take one of three forms:
@ -1275,21 +1299,61 @@ requirement is that it has the same arguments as the Django test runner:
If ``test_labels`` has a value of ``None``, the test runner should run If ``test_labels`` has a value of ``None``, the test runner should run
search for tests in all the applications in :setting:`INSTALLED_APPS`. search for tests in all the applications in :setting:`INSTALLED_APPS`.
``verbosity`` determines the amount of notification and debug information ``extra_tests`` is a list of extra ``TestCase`` instances to add to the
that will be printed to the console; ``0`` is no output, ``1`` is normal suite that is executed by the test runner. These extra tests are run
output, and ``2`` is verbose output. in addition to those discovered in the modules listed in ``test_labels``.
If ``interactive`` is ``True``, the test suite has permission to ask the This method should return the number of tests that failed.
user for instructions when the test suite is executed. An example of this
behavior would be asking for permission to delete an existing test .. method:: DjangoTestSuiteRunner.setup_test_environment()
database. If ``interactive`` is ``False``, the test suite must be able to
run without any manual intervention. Sets up the test environment ready for testing.
.. method:: DjangoTestSuiteRunner.build_suite(test_labels, extra_tests=[])
Constructs a test suite that matches the test labels provided.
``test_labels`` is a list of strings describing the tests to be run. A test
label can take one of three forms:
* ``app.TestCase.test_method`` -- Run a single test method in a test
case.
* ``app.TestCase`` -- Run all the test methods in a test case.
* ``app`` -- Search for and run all tests in the named application.
If ``test_labels`` has a value of ``None``, the test runner should run
search for tests in all the applications in :setting:`INSTALLED_APPS`.
``extra_tests`` is a list of extra ``TestCase`` instances to add to the ``extra_tests`` is a list of extra ``TestCase`` instances to add to the
suite that is executed by the test runner. These extra tests are run suite that is executed by the test runner. These extra tests are run
in addition to those discovered in the modules listed in ``module_list``. in addition to those discovered in the modules listed in ``test_labels``.
Returns a ``TestSuite`` instance ready to be run.
.. method:: DjangoTestSuiteRunner.setup_databases()
Creates the test databases.
Returns the list of old database names that will need to be restored
.. method:: DjangoTestSuiteRunner.run_suite(suite)
Runs the test suite.
Returns the result produced by the running the test suite.
.. method:: DjangoTestSuiteRunner.teardown_databases(old_names)
Destroys the test databases, restoring the old names.
.. method:: DjangoTestSuiteRunner.teardown_test_environment()
Restores the pre-test environment.
.. method:: DjangoTestSuiteRunner.suite_result(result)
Computes and returns a return code based on a test suite result.
This method should return the number of tests that failed.
Testing utilities Testing utilities
----------------- -----------------

View File

@ -156,11 +156,23 @@ def django_tests(verbosity, interactive, failfast, test_labels):
# Run the test suite, including the extra validation tests. # Run the test suite, including the extra validation tests.
from django.test.utils import get_runner from django.test.utils import get_runner
if not hasattr(settings, 'TEST_RUNNER'): if not hasattr(settings, 'TEST_RUNNER'):
settings.TEST_RUNNER = 'django.test.simple.run_tests' settings.TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner'
test_runner = get_runner(settings) TestRunner = get_runner(settings)
failures = test_runner(test_labels, verbosity=verbosity, interactive=interactive, failfast=failfast, if hasattr(TestRunner, 'func_name'):
# Pre 1.2 test runners were just functions,
# and did not support the 'failfast' option.
import warnings
warnings.warn(
'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.',
PendingDeprecationWarning
)
failures = TestRunner(test_labels, verbosity=verbosity, interactive=interactive,
extra_tests=extra_tests) extra_tests=extra_tests)
else:
test_runner = TestRunner(verbosity=verbosity, interactive=interactive, failfast=failfast)
failures = test_runner.run_tests(test_labels, extra_tests=extra_tests)
if failures: if failures:
sys.exit(bool(failures)) sys.exit(bool(failures))