Skip to content

Instantly share code, notes, and snippets.

Created December 9, 2011 04:07
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save carljm/1450104 to your computer and use it in GitHub Desktop.
Save carljm/1450104 to your computer and use it in GitHub Desktop.
Unittest2 test discovery and real dotted-path named test selection for Django
An alternative Django ``TEST_RUNNER`` which uses unittest2 test discovery from
a base path specified in settings, rather than requiring all tests to be in
``tests`` module of an app.
If you just run ``./ test``, it'll discover and run all tests
underneath the ``TEST_DISCOVERY_ROOT`` setting (a path). If you run
``./ test``, it'll run the tests in
that module (you can also pass multiple modules).
And (new in this updated version), if you give it a single dotted path to a
package, and that package does not itself directly contain any tests, it'll do
test discovery in all sub-modules of that package.
This code doesn't modify the default unittest2 test discovery behavior, which
only searches for tests in files named "test*.py".
from django.conf import settings
from django.test import TestCase
from django.test.simple import DjangoTestSuiteRunner, reorder_suite
from django.utils.importlib import import_module
from django.utils.unittest.loader import defaultTestLoader
class DiscoveryRunner(DjangoTestSuiteRunner):
"""A test suite runner that uses unittest2 test discovery."""
def build_suite(self, test_labels, extra_tests=None, **kwargs):
suite = None
discovery_root = settings.TEST_DISCOVERY_ROOT
if test_labels:
suite = defaultTestLoader.loadTestsFromNames(test_labels)
# if single named module has no tests, do discovery within it
if not suite.countTestCases() and len(test_labels) == 1:
suite = None
discovery_root = import_module(test_labels[0]).__path__[0]
if suite is None:
suite =
if extra_tests:
for test in extra_tests:
return reorder_suite(suite, (TestCase,))
You need the ``BASE_PATH`` and ``TEST_DISCOVERY_ROOT`` settings in order for
this test runner to work.
``BASE_PATH`` should be the directory containing your top-level package(s); in
other words, the directory that should be on ``sys.path`` for your code to
import. This is the directory containing ```` in the new Django 1.4
project layout.
``TEST_DISCOVERY_ROOT`` should be the root directory to discover tests
within. You could make this the same as ``BASE_PATH`` if you want tests to be
discovered anywhere in your project. If you want tests to only be discovered
within, say, a top-level ``tests`` directory, you'd set ``TEST_DISCOVERY_ROOT``
as shown below.
And you need to point the ``TEST_RUNNER`` setting to the ``DiscoveryRunner``
class above.
import os.path
# This is correct for the Django 1.4-style project layout; for the old-style
# project layout with ```` and ```` in the same directory,
# you'd want to only call ``os.path.dirname`` once.
BASE_PATH = os.path.dirname(os.path.dirname(__file__))
# This would be if you put all your tests within a top-level "tests" package.
TEST_DISCOVERY_ROOT = os.path.join(BASE_PATH, "tests")
# This assumes you place the above ``DiscoveryRunner`` in ``tests/``.
TEST_RUNNER = "tests.runner.DiscoveryRunner"
Copy link

carljm commented Mar 14, 2012

@chase-seibert Ah, I didn't think to document how unittest2 test discovery itself works. By default, only files named test*.py are searched for tests. You can change this by passing the pattern argument to the discover method (it takes a shell glob, the default value is "test*.py"). It would be easy enough to modify the above runner to respect a TEST_DISCOVERY_PATTERN setting.

Copy link

Nailed it. Thanks!

Copy link

Firstly, thanks for this. When I'm more hip I might use nose or something but this works perfectly.

Secondly there is a tiny mismatch between and DiscoveryRunner in settings and DiscoveryDjangoTestSuiteRunner in Tripped me up for a few seconds.

Copy link

carljm commented May 14, 2012

Thanks @mallison, fixed.

Nose (with django-nose, so you don't lose the Django db setup/teardown stuff) is a great option, too. The main reason for this gist is that it's pre-testing something we're hoping to put in Django core in time for 1.5, and requiring nose isn't really an option for core.

Copy link

jezdez commented May 20, 2012

In case anyone is interested, I've packaged this up for general consumption (apologies @carljm, I needed it quick):

Copy link

tartley commented Jul 12, 2012

I'm stuck on how to use this with Jenkins. If I want my Jenkins server to use this plugin, then I replace the "./ jenkins" command that Jenkins was running with "./ test". But now I'm no longer using the django-jenkins plugin command, I don't get the XML output that I think Jenkins needs in order to display the test results. Am I being dense?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment