Skip to content

Commit 650cea9

Browse files
committed
Fixed #4460 -- Added the ability to be more specific in the test cases 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
1 parent 5b8d2c9 commit 650cea9

File tree

4 files changed

+119
-57
lines changed

4 files changed

+119
-57
lines changed

django/core/management.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1331,16 +1331,11 @@ def runfcgi(args):
13311331
runfastcgi(args)
13321332
runfcgi.args = '[various KEY=val options, use `runfcgi help` for help]'
13331333

1334-
def test(app_labels, verbosity=1, interactive=True):
1334+
def test(test_labels, verbosity=1, interactive=True):
13351335
"Runs the test suite for the specified applications"
13361336
from django.conf import settings
13371337
from django.db.models import get_app, get_apps
1338-
1339-
if len(app_labels) == 0:
1340-
app_list = get_apps()
1341-
else:
1342-
app_list = [get_app(app_label) for app_label in app_labels]
1343-
1338+
13441339
test_path = settings.TEST_RUNNER.split('.')
13451340
# Allow for Python 2.5 relative paths
13461341
if len(test_path) > 1:
@@ -1350,7 +1345,7 @@ def test(app_labels, verbosity=1, interactive=True):
13501345
test_module = __import__(test_module_name, {}, {}, test_path[-1])
13511346
test_runner = getattr(test_module, test_path[-1])
13521347

1353-
failures = test_runner(app_list, verbosity=verbosity, interactive=interactive)
1348+
failures = test_runner(test_labels, verbosity=verbosity, interactive=interactive)
13541349
if failures:
13551350
sys.exit(failures)
13561351

django/test/simple.py

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import unittest
22
from django.conf import settings
3+
from django.db.models import get_app, get_apps
34
from django.test import _doctest as doctest
45
from django.test.utils import setup_test_environment, teardown_test_environment
56
from django.test.utils import create_test_db, destroy_test_db
@@ -10,6 +11,31 @@
1011

1112
doctestOutputChecker = OutputChecker()
1213

14+
def get_tests(app_module):
15+
try:
16+
app_path = app_module.__name__.split('.')[:-1]
17+
test_module = __import__('.'.join(app_path + [TEST_MODULE]), {}, {}, TEST_MODULE)
18+
except ImportError, e:
19+
# Couldn't import tests.py. Was it due to a missing file, or
20+
# due to an import error in a tests.py that actually exists?
21+
import os.path
22+
from imp import find_module
23+
try:
24+
mod = find_module(TEST_MODULE, [os.path.dirname(app_module.__file__)])
25+
except ImportError:
26+
# 'tests' module doesn't exist. Move on.
27+
test_module = None
28+
else:
29+
# The module exists, so there must be an import error in the
30+
# test module itself. We don't need the module; so if the
31+
# module was a single file module (i.e., tests.py), close the file
32+
# handle returned by find_module. Otherwise, the test module
33+
# is a directory, and there is nothing to close.
34+
if mod[0]:
35+
mod[0].close()
36+
raise
37+
return test_module
38+
1339
def build_suite(app_module):
1440
"Create a complete Django test suite for the provided application module"
1541
suite = unittest.TestSuite()
@@ -30,10 +56,8 @@ def build_suite(app_module):
3056

3157
# Check to see if a separate 'tests' module exists parallel to the
3258
# models module
33-
try:
34-
app_path = app_module.__name__.split('.')[:-1]
35-
test_module = __import__('.'.join(app_path + [TEST_MODULE]), {}, {}, TEST_MODULE)
36-
59+
test_module = get_tests(app_module)
60+
if test_module:
3761
# Load unit and doctests in the tests.py module. If module has
3862
# a suite() method, use it. Otherwise build the test suite ourselves.
3963
if hasattr(test_module, 'suite'):
@@ -47,34 +71,50 @@ def build_suite(app_module):
4771
except ValueError:
4872
# No doc tests in tests.py
4973
pass
50-
except ImportError, e:
51-
# Couldn't import tests.py. Was it due to a missing file, or
52-
# due to an import error in a tests.py that actually exists?
53-
import os.path
54-
from imp import find_module
55-
try:
56-
mod = find_module(TEST_MODULE, [os.path.dirname(app_module.__file__)])
57-
except ImportError:
58-
# 'tests' module doesn't exist. Move on.
59-
pass
60-
else:
61-
# The module exists, so there must be an import error in the
62-
# test module itself. We don't need the module; so if the
63-
# module was a single file module (i.e., tests.py), close the file
64-
# handle returned by find_module. Otherwise, the test module
65-
# is a directory, and there is nothing to close.
66-
if mod[0]:
67-
mod[0].close()
68-
raise
69-
7074
return suite
7175

72-
def run_tests(module_list, verbosity=1, interactive=True, extra_tests=[]):
76+
def build_test(label):
77+
"""Construct a test case a test with the specified label. Label should
78+
be of the form model.TestClass or model.TestClass.test_method. Returns
79+
an instantiated test or test suite corresponding to the label provided.
80+
81+
"""
82+
parts = label.split('.')
83+
if len(parts) < 2 or len(parts) > 3:
84+
raise ValueError("Test label '%s' should be of the form app.TestCase or app.TestCase.test_method" % label)
85+
86+
app_module = get_app(parts[0])
87+
TestClass = getattr(app_module, parts[1], None)
88+
89+
# Couldn't find the test class in models.py; look in tests.py
90+
if TestClass is None:
91+
test_module = get_tests(app_module)
92+
if test_module:
93+
TestClass = getattr(test_module, parts[1], None)
94+
95+
if len(parts) == 2: # label is app.TestClass
96+
try:
97+
return unittest.TestLoader().loadTestsFromTestCase(TestClass)
98+
except TypeError:
99+
raise ValueError("Test label '%s' does not refer to a test class" % label)
100+
else: # label is app.TestClass.test_method
101+
return TestClass(parts[2])
102+
103+
def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[]):
73104
"""
74-
Run the unit tests for all the modules in the provided list.
75-
This testrunner will search each of the modules in the provided list,
76-
looking for doctests and unittests in models.py or tests.py within
77-
the module. A list of 'extra' tests may also be provided; these tests
105+
Run the unit tests for all the test labels in the provided list.
106+
Labels must be of the form:
107+
- app.TestClass.test_method
108+
Run a single specific test method
109+
- app.TestClass
110+
Run all the test methods in a given class
111+
- app
112+
Search for doctests and unittests in the named application.
113+
114+
When looking for tests, the test runner will look in the models and
115+
tests modules for the application.
116+
117+
A list of 'extra' tests may also be provided; these tests
78118
will be added to the test suite.
79119
80120
Returns the number of tests that failed.
@@ -83,9 +123,17 @@ def run_tests(module_list, verbosity=1, interactive=True, extra_tests=[]):
83123

84124
settings.DEBUG = False
85125
suite = unittest.TestSuite()
86-
87-
for module in module_list:
88-
suite.addTest(build_suite(module))
126+
127+
if test_labels:
128+
for label in test_labels:
129+
if '.' in label:
130+
suite.addTest(build_test(label))
131+
else:
132+
app = get_app(label)
133+
suite.addTest(build_suite(app))
134+
else:
135+
for app in get_apps():
136+
suite.addTest(build_suite(app))
89137

90138
for test in extra_tests:
91139
suite.addTest(test)

docs/testing.txt

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,9 @@ look like::
450450
def setUp(self):
451451
# test definitions as before
452452

453+
def testFluffyAnimals(self):
454+
# A test that uses the fixtures
455+
453456
At the start of each test case, before ``setUp()`` is run, Django will
454457
flush the database, returning the database the state it was in directly
455458
after ``syncdb`` was called. Then, all the named fixtures are installed.
@@ -483,8 +486,8 @@ that can be useful in testing the behavior of web sites.
483486

484487
``assertContains(response, text, count=None, status_code=200)``
485488
Assert that a response indicates that a page could be retrieved and
486-
produced the nominated status code, and that ``text`` in the content
487-
of the response. If ``count`` is provided, ``text`` must occur exactly
489+
produced the nominated status code, and that ``text`` in the content
490+
of the response. If ``count`` is provided, ``text`` must occur exactly
488491
``count`` times in the response.
489492

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

572575
$ ./manage.py test animals
573576

577+
**New in Django development version:** If you use unit tests, you can be more
578+
specific in the tests that are executed. To run a single test case in an
579+
application (for example, the AnimalTestCase described previously), add the
580+
name of the test case to the label on the command line::
581+
582+
$ ./manage.py test animals.AnimalTestCase
583+
584+
**New in Django development version:**To run a single test method inside a
585+
test case, add the name of the test method to the label::
586+
587+
$ ./manage.py test animals.AnimalTestCase.testFluffyAnimals
588+
574589
When you run your tests, you'll see a bunch of text flow by as the test
575590
database is created and models are initialized. This test database is
576591
created from scratch every time you run your tests.
@@ -665,25 +680,30 @@ By convention, a test runner should be called ``run_tests``; however, you
665680
can call it anything you want. The only requirement is that it has the
666681
same arguments as the Django test runner:
667682

668-
``run_tests(module_list, verbosity=1, interactive=True, extra_tests=[])``
669-
The module list is the list of Python modules that contain the models to be
670-
tested. This is the same format returned by ``django.db.models.get_apps()``.
671-
The test runner should search these modules for tests to execute.
683+
``run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[])``
684+
**New in Django development version:** ``test_labels`` is a list of
685+
strings describing the tests to be run. A test label can take one of
686+
three forms:
687+
* ``app.TestCase.test_method`` - Run a single test method in a test case
688+
* ``app.TestCase`` - Run all the test methods in a test case
689+
* ``app`` - Search for and run all tests in the named application.
690+
If ``test_labels`` has a value of ``None``, the test runner should run
691+
search for tests in all the applications in ``INSTALLED_APPS``.
672692

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

677-
**New in Django development version** If ``interactive`` is ``True``, the
697+
**New in Django development version:** If ``interactive`` is ``True``, the
678698
test suite may ask the user for instructions when the test suite is
679699
executed. An example of this behavior would be asking for permission to
680-
delete an existing test database. If ``interactive`` is ``False, the
700+
delete an existing test database. If ``interactive`` is ``False, the
681701
test suite must be able to run without any manual intervention.
682-
683-
``extra_tests`` is a list of extra ``TestCase`` instances to add to the
684-
suite that is executed by the test runner. These extra tests are run
702+
703+
``extra_tests`` is a list of extra ``TestCase`` instances to add to the
704+
suite that is executed by the test runner. These extra tests are run
685705
in addition to those discovered in the modules listed in ``module_list``.
686-
706+
687707
This method should return the number of tests that failed.
688708

689709
Testing utilities

tests/runtests.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def runTest(self):
7373
self.assert_(not unexpected, "Unexpected Errors: " + '\n'.join(unexpected))
7474
self.assert_(not missing, "Missing Errors: " + '\n'.join(missing))
7575

76-
def django_tests(verbosity, interactive, tests_to_run):
76+
def django_tests(verbosity, interactive, test_labels):
7777
from django.conf import settings
7878

7979
old_installed_apps = settings.INSTALLED_APPS
@@ -109,14 +109,13 @@ def django_tests(verbosity, interactive, tests_to_run):
109109
# if the model was named on the command line, or
110110
# no models were named (i.e., run all), import
111111
# this model and add it to the list to test.
112-
if not tests_to_run or model_name in tests_to_run:
112+
if not test_labels or model_name in set([label.split('.')[0] for label in test_labels]):
113113
if verbosity >= 1:
114114
print "Importing model %s" % model_name
115115
mod = load_app(model_label)
116116
if mod:
117117
if model_label not in settings.INSTALLED_APPS:
118118
settings.INSTALLED_APPS.append(model_label)
119-
test_models.append(mod)
120119
except Exception, e:
121120
sys.stderr.write("Error while importing %s:" % model_name + ''.join(traceback.format_exception(*sys.exc_info())[1:]))
122121
continue
@@ -125,12 +124,12 @@ def django_tests(verbosity, interactive, tests_to_run):
125124
extra_tests = []
126125
for model_dir, model_name in get_invalid_models():
127126
model_label = '.'.join([model_dir, model_name])
128-
if not tests_to_run or model_name in tests_to_run:
127+
if not test_labels or model_name in test_labels:
129128
extra_tests.append(InvalidModelTestCase(model_label))
130129

131130
# Run the test suite, including the extra validation tests.
132131
from django.test.simple import run_tests
133-
failures = run_tests(test_models, verbosity=verbosity, interactive=interactive, extra_tests=extra_tests)
132+
failures = run_tests(test_labels, verbosity=verbosity, interactive=interactive, extra_tests=extra_tests)
134133
if failures:
135134
sys.exit(failures)
136135

0 commit comments

Comments
 (0)