From 650cea9170313d9a2197d4050f6dcabaaeaa3b20 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 28 Jul 2007 04:02:52 +0000 Subject: [PATCH] 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 --- django/core/management.py | 11 +--- django/test/simple.py | 112 +++++++++++++++++++++++++++----------- docs/testing.txt | 44 +++++++++++---- tests/runtests.py | 9 ++- 4 files changed, 119 insertions(+), 57 deletions(-) diff --git a/django/core/management.py b/django/core/management.py index 4862a7833fb..edf11ce478f 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -1331,16 +1331,11 @@ def runfcgi(args): runfastcgi(args) 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" from django.conf import settings 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('.') # Allow for Python 2.5 relative paths if len(test_path) > 1: @@ -1350,7 +1345,7 @@ def test(app_labels, verbosity=1, interactive=True): test_module = __import__(test_module_name, {}, {}, 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: sys.exit(failures) diff --git a/django/test/simple.py b/django/test/simple.py index 08e27429e4e..6fa381ac4d5 100644 --- a/django/test/simple.py +++ b/django/test/simple.py @@ -1,5 +1,6 @@ import unittest from django.conf import settings +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.utils import create_test_db, destroy_test_db @@ -10,6 +11,31 @@ TEST_MODULE = 'tests' 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): "Create a complete Django test suite for the provided application module" suite = unittest.TestSuite() @@ -30,10 +56,8 @@ def build_suite(app_module): # Check to see if a separate 'tests' module exists parallel to the # models module - try: - app_path = app_module.__name__.split('.')[:-1] - test_module = __import__('.'.join(app_path + [TEST_MODULE]), {}, {}, TEST_MODULE) - + test_module = get_tests(app_module) + if test_module: # Load unit and doctests in the tests.py module. If module has # a suite() method, use it. Otherwise build the test suite ourselves. if hasattr(test_module, 'suite'): @@ -47,34 +71,50 @@ def build_suite(app_module): except ValueError: # No doc tests in tests.py 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 -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. + """ - Run the unit tests for all the modules in the provided list. - This testrunner will search each of the modules in the provided list, - looking for doctests and unittests in models.py or tests.py within - the module. A list of 'extra' tests may also be provided; these tests + 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 test labels in the provided list. + Labels must be of the form: + - app.TestClass.test_method + 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. Returns the number of tests that failed. @@ -83,9 +123,17 @@ def run_tests(module_list, verbosity=1, interactive=True, extra_tests=[]): settings.DEBUG = False suite = unittest.TestSuite() - - for module in module_list: - suite.addTest(build_suite(module)) + + 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)) for test in extra_tests: suite.addTest(test) diff --git a/docs/testing.txt b/docs/testing.txt index fb472a2cc2a..7f47e90b19c 100644 --- a/docs/testing.txt +++ b/docs/testing.txt @@ -450,6 +450,9 @@ look like:: def setUp(self): # 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 flush the database, returning the database the state it was in directly 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. ``assertContains(response, text, count=None, status_code=200)`` Assert that a response indicates that a page could be retrieved and - produced the nominated status code, and that ``text`` in the content - of the response. If ``count`` is provided, ``text`` must occur exactly + produced the nominated status code, and that ``text`` in the content + of the response. If ``count`` is provided, ``text`` must occur exactly ``count`` times in the response. ``assertFormError(response, form, field, errors)`` @@ -571,6 +574,18 @@ but you only want to run the animals unit tests, run:: $ ./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 database is created and models are initialized. This test database is 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 can call it anything you want. The only requirement is that it has the same arguments as the Django test runner: -``run_tests(module_list, verbosity=1, interactive=True, extra_tests=[])`` - The module list is the list of Python modules that contain the models to be - tested. This is the same format returned by ``django.db.models.get_apps()``. - The test runner should search these modules for tests to execute. +``run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[])`` + **New in Django development version:** ``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 ``INSTALLED_APPS``. 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. - **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 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. - - ``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 + + ``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 in addition to those discovered in the modules listed in ``module_list``. - + This method should return the number of tests that failed. Testing utilities diff --git a/tests/runtests.py b/tests/runtests.py index 7b52445d84b..56679ecb7c6 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -73,7 +73,7 @@ class InvalidModelTestCase(unittest.TestCase): self.assert_(not unexpected, "Unexpected Errors: " + '\n'.join(unexpected)) 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 old_installed_apps = settings.INSTALLED_APPS @@ -109,14 +109,13 @@ def django_tests(verbosity, interactive, tests_to_run): # if the model was named on the command line, or # no models were named (i.e., run all), import # 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: print "Importing model %s" % model_name mod = load_app(model_label) if mod: if model_label not in settings.INSTALLED_APPS: settings.INSTALLED_APPS.append(model_label) - test_models.append(mod) except Exception, e: sys.stderr.write("Error while importing %s:" % model_name + ''.join(traceback.format_exception(*sys.exc_info())[1:])) continue @@ -125,12 +124,12 @@ def django_tests(verbosity, interactive, tests_to_run): extra_tests = [] for model_dir, model_name in get_invalid_models(): 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)) # Run the test suite, including the extra validation 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: sys.exit(failures)