Skip to content

Commit

Permalink
Fixed #4460 -- Added the ability to be more specific in the test case…
Browse files Browse the repository at this point in the history
…s that are executed. This is a backwards incompatible change for any user with a custom test runner. See the wiki for details.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@5769 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
freakboy3742 committed Jul 28, 2007
1 parent 5b8d2c9 commit 650cea9
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 57 deletions.
11 changes: 3 additions & 8 deletions django/core/management.py
Expand Up @@ -1331,16 +1331,11 @@ def runfcgi(args):
runfastcgi(args) runfastcgi(args)
runfcgi.args = '[various KEY=val options, use `runfcgi help` for help]' runfcgi.args = '[various KEY=val options, use `runfcgi help` for help]'


def test(app_labels, verbosity=1, interactive=True): def test(test_labels, verbosity=1, interactive=True):
"Runs the test suite for the specified applications" "Runs the test suite for the specified applications"
from django.conf import settings from django.conf import settings
from django.db.models import get_app, get_apps from django.db.models import get_app, get_apps


if len(app_labels) == 0:
app_list = get_apps()
else:
app_list = [get_app(app_label) for app_label in app_labels]

test_path = settings.TEST_RUNNER.split('.') test_path = settings.TEST_RUNNER.split('.')
# Allow for Python 2.5 relative paths # Allow for Python 2.5 relative paths
if len(test_path) > 1: if len(test_path) > 1:
Expand All @@ -1350,7 +1345,7 @@ def test(app_labels, verbosity=1, interactive=True):
test_module = __import__(test_module_name, {}, {}, test_path[-1]) test_module = __import__(test_module_name, {}, {}, test_path[-1])
test_runner = getattr(test_module, test_path[-1]) test_runner = getattr(test_module, test_path[-1])


failures = test_runner(app_list, verbosity=verbosity, interactive=interactive) failures = test_runner(test_labels, verbosity=verbosity, interactive=interactive)
if failures: if failures:
sys.exit(failures) sys.exit(failures)


Expand Down
112 changes: 80 additions & 32 deletions django/test/simple.py
@@ -1,5 +1,6 @@
import unittest import unittest
from django.conf import settings from django.conf import settings
from django.db.models import get_app, get_apps
from django.test import _doctest as doctest from django.test import _doctest as doctest
from django.test.utils import setup_test_environment, teardown_test_environment from django.test.utils import setup_test_environment, teardown_test_environment
from django.test.utils import create_test_db, destroy_test_db from django.test.utils import create_test_db, destroy_test_db
Expand All @@ -10,6 +11,31 @@


doctestOutputChecker = OutputChecker() doctestOutputChecker = OutputChecker()


def get_tests(app_module):
try:
app_path = app_module.__name__.split('.')[:-1]
test_module = __import__('.'.join(app_path + [TEST_MODULE]), {}, {}, TEST_MODULE)
except ImportError, e:
# Couldn't import tests.py. Was it due to a missing file, or
# due to an import error in a tests.py that actually exists?
import os.path
from imp import find_module
try:
mod = find_module(TEST_MODULE, [os.path.dirname(app_module.__file__)])
except ImportError:
# 'tests' module doesn't exist. Move on.
test_module = None
else:
# The module exists, so there must be an import error in the
# test module itself. We don't need the module; so if the
# module was a single file module (i.e., tests.py), close the file
# handle returned by find_module. Otherwise, the test module
# is a directory, and there is nothing to close.
if mod[0]:
mod[0].close()
raise
return test_module

def build_suite(app_module): def build_suite(app_module):
"Create a complete Django test suite for the provided application module" "Create a complete Django test suite for the provided application module"
suite = unittest.TestSuite() suite = unittest.TestSuite()
Expand All @@ -30,10 +56,8 @@ def build_suite(app_module):


# Check to see if a separate 'tests' module exists parallel to the # Check to see if a separate 'tests' module exists parallel to the
# models module # models module
try: test_module = get_tests(app_module)
app_path = app_module.__name__.split('.')[:-1] if test_module:
test_module = __import__('.'.join(app_path + [TEST_MODULE]), {}, {}, TEST_MODULE)

# Load unit and doctests in the tests.py module. If module has # Load unit and doctests in the tests.py module. If module has
# a suite() method, use it. Otherwise build the test suite ourselves. # a suite() method, use it. Otherwise build the test suite ourselves.
if hasattr(test_module, 'suite'): if hasattr(test_module, 'suite'):
Expand All @@ -47,34 +71,50 @@ def build_suite(app_module):
except ValueError: except ValueError:
# No doc tests in tests.py # No doc tests in tests.py
pass pass
except ImportError, e:
# Couldn't import tests.py. Was it due to a missing file, or
# due to an import error in a tests.py that actually exists?
import os.path
from imp import find_module
try:
mod = find_module(TEST_MODULE, [os.path.dirname(app_module.__file__)])
except ImportError:
# 'tests' module doesn't exist. Move on.
pass
else:
# The module exists, so there must be an import error in the
# test module itself. We don't need the module; so if the
# module was a single file module (i.e., tests.py), close the file
# handle returned by find_module. Otherwise, the test module
# is a directory, and there is nothing to close.
if mod[0]:
mod[0].close()
raise

return suite return suite


def run_tests(module_list, verbosity=1, interactive=True, extra_tests=[]): def build_test(label):
"""Construct a test case a test with the specified label. Label should
be of the form model.TestClass or model.TestClass.test_method. Returns
an instantiated test or test suite corresponding to the label provided.
"""
parts = label.split('.')
if len(parts) < 2 or len(parts) > 3:
raise ValueError("Test label '%s' should be of the form app.TestCase or app.TestCase.test_method" % label)

app_module = get_app(parts[0])
TestClass = getattr(app_module, parts[1], None)

# Couldn't find the test class in models.py; look in tests.py
if TestClass is None:
test_module = get_tests(app_module)
if test_module:
TestClass = getattr(test_module, parts[1], None)

if len(parts) == 2: # label is app.TestClass
try:
return unittest.TestLoader().loadTestsFromTestCase(TestClass)
except TypeError:
raise ValueError("Test label '%s' does not refer to a test class" % label)
else: # label is app.TestClass.test_method
return TestClass(parts[2])

def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[]):
""" """
Run the unit tests for all the modules in the provided list. Run the unit tests for all the test labels in the provided list.
This testrunner will search each of the modules in the provided list, Labels must be of the form:
looking for doctests and unittests in models.py or tests.py within - app.TestClass.test_method
the module. A list of 'extra' tests may also be provided; these tests Run a single specific test method
- app.TestClass
Run all the test methods in a given class
- app
Search for doctests and unittests in the named application.
When looking for tests, the test runner will look in the models and
tests modules for the application.
A list of 'extra' tests may also be provided; these tests
will be added to the test suite. will be added to the test suite.
Returns the number of tests that failed. Returns the number of tests that failed.
Expand All @@ -83,9 +123,17 @@ def run_tests(module_list, verbosity=1, interactive=True, extra_tests=[]):


settings.DEBUG = False settings.DEBUG = False
suite = unittest.TestSuite() suite = unittest.TestSuite()


for module in module_list: if test_labels:
suite.addTest(build_suite(module)) 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: for test in extra_tests:
suite.addTest(test) suite.addTest(test)
Expand Down
44 changes: 32 additions & 12 deletions docs/testing.txt
Expand Up @@ -450,6 +450,9 @@ look like::
def setUp(self): def setUp(self):
# test definitions as before # test definitions as before


def testFluffyAnimals(self):
# A test that uses the fixtures

At the start of each test case, before ``setUp()`` is run, Django will At the start of each test case, before ``setUp()`` is run, Django will
flush the database, returning the database the state it was in directly flush the database, returning the database the state it was in directly
after ``syncdb`` was called. Then, all the named fixtures are installed. after ``syncdb`` was called. Then, all the named fixtures are installed.
Expand Down Expand Up @@ -483,8 +486,8 @@ that can be useful in testing the behavior of web sites.


``assertContains(response, text, count=None, status_code=200)`` ``assertContains(response, text, count=None, status_code=200)``
Assert that a response indicates that a page could be retrieved and Assert that a response indicates that a page could be retrieved and
produced the nominated status code, and that ``text`` in the content produced the nominated status code, and that ``text`` in the content
of the response. If ``count`` is provided, ``text`` must occur exactly of the response. If ``count`` is provided, ``text`` must occur exactly
``count`` times in the response. ``count`` times in the response.


``assertFormError(response, form, field, errors)`` ``assertFormError(response, form, field, errors)``
Expand Down Expand Up @@ -571,6 +574,18 @@ but you only want to run the animals unit tests, run::


$ ./manage.py test animals $ ./manage.py test animals


**New in Django development version:** If you use unit tests, you can be more
specific in the tests that are executed. To run a single test case in an
application (for example, the AnimalTestCase described previously), add the
name of the test case to the label on the command line::

$ ./manage.py test animals.AnimalTestCase

**New in Django development version:**To run a single test method inside a
test case, add the name of the test method to the label::

$ ./manage.py test animals.AnimalTestCase.testFluffyAnimals

When you run your tests, you'll see a bunch of text flow by as the test When you run your tests, you'll see a bunch of text flow by as the test
database is created and models are initialized. This test database is database is created and models are initialized. This test database is
created from scratch every time you run your tests. created from scratch every time you run your tests.
Expand Down Expand Up @@ -665,25 +680,30 @@ By convention, a test runner should be called ``run_tests``; however, you
can call it anything you want. The only requirement is that it has the can call it anything you want. The only requirement is that it has the
same arguments as the Django test runner: same arguments as the Django test runner:


``run_tests(module_list, verbosity=1, interactive=True, extra_tests=[])`` ``run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[])``
The module list is the list of Python modules that contain the models to be **New in Django development version:** ``test_labels`` is a list of
tested. This is the same format returned by ``django.db.models.get_apps()``. strings describing the tests to be run. A test label can take one of
The test runner should search these modules for tests to execute. 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 ``INSTALLED_APPS``.


Verbosity determines the amount of notification and debug information that Verbosity determines the amount of notification and debug information that
will be printed to the console; ``0`` is no output, ``1`` is normal output, will be printed to the console; ``0`` is no output, ``1`` is normal output,
and ``2`` is verbose output. and ``2`` is verbose output.


**New in Django development version** If ``interactive`` is ``True``, the **New in Django development version:** If ``interactive`` is ``True``, the
test suite may ask the user for instructions when the test suite is test suite may ask the user for instructions when the test suite is
executed. An example of this behavior would be asking for permission to executed. An example of this behavior would be asking for permission to
delete an existing test database. If ``interactive`` is ``False, the delete an existing test database. If ``interactive`` is ``False, the
test suite must be able to run without any manual intervention. test suite must be able to run without any manual intervention.

``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 ``module_list``.

This method should return the number of tests that failed. This method should return the number of tests that failed.


Testing utilities Testing utilities
Expand Down
9 changes: 4 additions & 5 deletions tests/runtests.py
Expand Up @@ -73,7 +73,7 @@ def runTest(self):
self.assert_(not unexpected, "Unexpected Errors: " + '\n'.join(unexpected)) self.assert_(not unexpected, "Unexpected Errors: " + '\n'.join(unexpected))
self.assert_(not missing, "Missing Errors: " + '\n'.join(missing)) self.assert_(not missing, "Missing Errors: " + '\n'.join(missing))


def django_tests(verbosity, interactive, tests_to_run): def django_tests(verbosity, interactive, test_labels):
from django.conf import settings from django.conf import settings


old_installed_apps = settings.INSTALLED_APPS old_installed_apps = settings.INSTALLED_APPS
Expand Down Expand Up @@ -109,14 +109,13 @@ def django_tests(verbosity, interactive, tests_to_run):
# if the model was named on the command line, or # if the model was named on the command line, or
# no models were named (i.e., run all), import # no models were named (i.e., run all), import
# this model and add it to the list to test. # this model and add it to the list to test.
if not tests_to_run or model_name in tests_to_run: if not test_labels or model_name in set([label.split('.')[0] for label in test_labels]):
if verbosity >= 1: if verbosity >= 1:
print "Importing model %s" % model_name print "Importing model %s" % model_name
mod = load_app(model_label) mod = load_app(model_label)
if mod: if mod:
if model_label not in settings.INSTALLED_APPS: if model_label not in settings.INSTALLED_APPS:
settings.INSTALLED_APPS.append(model_label) settings.INSTALLED_APPS.append(model_label)
test_models.append(mod)
except Exception, e: except Exception, e:
sys.stderr.write("Error while importing %s:" % model_name + ''.join(traceback.format_exception(*sys.exc_info())[1:])) sys.stderr.write("Error while importing %s:" % model_name + ''.join(traceback.format_exception(*sys.exc_info())[1:]))
continue continue
Expand All @@ -125,12 +124,12 @@ def django_tests(verbosity, interactive, tests_to_run):
extra_tests = [] extra_tests = []
for model_dir, model_name in get_invalid_models(): for model_dir, model_name in get_invalid_models():
model_label = '.'.join([model_dir, model_name]) model_label = '.'.join([model_dir, model_name])
if not tests_to_run or model_name in tests_to_run: if not test_labels or model_name in test_labels:
extra_tests.append(InvalidModelTestCase(model_label)) extra_tests.append(InvalidModelTestCase(model_label))


# Run the test suite, including the extra validation tests. # Run the test suite, including the extra validation tests.
from django.test.simple import run_tests from django.test.simple import run_tests
failures = run_tests(test_models, verbosity=verbosity, interactive=interactive, extra_tests=extra_tests) failures = run_tests(test_labels, verbosity=verbosity, interactive=interactive, extra_tests=extra_tests)
if failures: if failures:
sys.exit(failures) sys.exit(failures)


Expand Down

0 comments on commit 650cea9

Please sign in to comment.