Merged revisions 72570,72582-72583,73027,73049,73071,73151,73247 via svnmerge from

svn+ssh://pythondev@svn.python.org/python/trunk

........
  r72570 | michael.foord | 2009-05-11 12:59:43 -0500 (Mon, 11 May 2009) | 7 lines

  Adds a verbosity keyword argument to unittest.main plus a minor fix allowing you to specify test modules / classes
  from the command line.

  Closes issue 5995.

  Michael Foord
........
  r72582 | michael.foord | 2009-05-12 05:46:23 -0500 (Tue, 12 May 2009) | 1 line

  Fix to restore command line behaviour for test modules using unittest.main(). Regression caused by issue 5995. Michael
........
  r72583 | michael.foord | 2009-05-12 05:49:13 -0500 (Tue, 12 May 2009) | 1 line

  Better fix for modules using unittest.main(). Fixes regression caused by commit for issue 5995. Michael Foord
........
  r73027 | michael.foord | 2009-05-29 15:33:46 -0500 (Fri, 29 May 2009) | 1 line

  Add test discovery to unittest. Issue 6001.
........
  r73049 | georg.brandl | 2009-05-30 05:45:40 -0500 (Sat, 30 May 2009) | 1 line

  Rewrap a few long lines.
........
  r73071 | georg.brandl | 2009-05-31 09:15:25 -0500 (Sun, 31 May 2009) | 1 line

  Fix markup.
........
  r73151 | michael.foord | 2009-06-02 13:08:27 -0500 (Tue, 02 Jun 2009) | 1 line

  Restore default testRunner argument in unittest.main to None. Issue 6177
........
  r73247 | michael.foord | 2009-06-05 09:14:34 -0500 (Fri, 05 Jun 2009) | 1 line

  Fix unittest discovery tests for Windows. Issue 6199
........
This commit is contained in:
Benjamin Peterson 2009-06-27 23:45:02 +00:00
parent f7a6b508ce
commit d2397753ee
3 changed files with 709 additions and 55 deletions

View File

@ -78,15 +78,82 @@ need to derive from a specific class.
Another test-support module with a very different flavor. Another test-support module with a very different flavor.
`Simple Smalltalk Testing: With Patterns <http://www.XProgramming.com/testfram.htm>`_ `Simple Smalltalk Testing: With Patterns <http://www.XProgramming.com/testfram.htm>`_
Kent Beck's original paper on testing frameworks using the pattern shared by Kent Beck's original paper on testing frameworks using the pattern shared
:mod:`unittest`. by :mod:`unittest`.
`Nose <http://code.google.com/p/python-nose/>`_ and `py.test <http://pytest.org>`_ `Nose <http://code.google.com/p/python-nose/>`_ and `py.test <http://pytest.org>`_
Third-party unittest frameworks with a lighter-weight syntax Third-party unittest frameworks with a lighter-weight syntax for writing
for writing tests. For example, ``assert func(10) == 42``. tests. For example, ``assert func(10) == 42``.
`python-mock <http://python-mock.sourceforge.net/>`_ and `minimock <http://blog.ianbicking.org/minimock.html>`_ `python-mock <http://python-mock.sourceforge.net/>`_ and `minimock <http://blog.ianbicking.org/minimock.html>`_
Tools for creating mock test objects (objects simulating external resources). Tools for creating mock test objects (objects simulating external
resources).
.. _unittest-command-line-interface:
Command Line Interface
----------------------
The unittest module can be used from the command line to run tests from
modules, classes or even individual test methods::
python -m unittest test_module1 test_module2
python -m unittest test_module.TestClass
python -m unittest test_module.TestClass.test_method
You can pass in a list with any combination of module names, and fully
qualified class or method names.
You can run tests with more detail (higher verbosity) by passing in the -v flag::
python-m unittest -v test_module
For a list of all the command line options::
python -m unittest -h
.. versionchanged:: 2.7
In earlier versions it was only possible to run individual test methods and
not modules or classes.
The command line can also be used for test discovery, for running all of the
tests in a project or just a subset.
.. _unittest-test-discovery:
Test Discovery
--------------
.. versionadded:: 2.7
unittest supports simple test discovery. For a project's tests to be
compatible with test discovery they must all be importable from the top level
directory of the project; i.e. they must all be in Python packages.
Test discovery is implemented in :meth:`TestLoader.discover`, but can also be
used from the command line. The basic command line usage is::
cd project_directory
python -m unittest discover
The ``discover`` sub-command has the following options:
-v, --verbose Verbose output
-s directory Directory to start discovery ('.' default)
-p pattern Pattern to match test files ('test*.py' default)
-t directory Top level directory of project (default to
start directory)
The -s, -p, & -t options can be passsed in as positional arguments. The
following two command lines are equivalent::
python -m unittest -s project_directory -p '*_test.py'
python -m unittest project_directory '*_test.py'
Test modules and packages can customize test loading and discovery by through
the `load_tests protocol`_.
.. _unittest-minimal-example: .. _unittest-minimal-example:
@ -175,7 +242,6 @@ The above examples show the most commonly used :mod:`unittest` features which
are sufficient to meet many everyday testing needs. The remainder of the are sufficient to meet many everyday testing needs. The remainder of the
documentation explores the full feature set from first principles. documentation explores the full feature set from first principles.
.. _organizing-tests: .. _organizing-tests:
Organizing test code Organizing test code
@ -206,13 +272,12 @@ The simplest :class:`TestCase` subclass will simply override the
self.assertEqual(widget.size(), (50, 50), 'incorrect default size') self.assertEqual(widget.size(), (50, 50), 'incorrect default size')
Note that in order to test something, we use the one of the :meth:`assert\*` Note that in order to test something, we use the one of the :meth:`assert\*`
methods provided by the :class:`TestCase` base class. If the methods provided by the :class:`TestCase` base class. If the test fails, an
test fails, an exception will be raised, and :mod:`unittest` will identify the exception will be raised, and :mod:`unittest` will identify the test case as a
test case as a :dfn:`failure`. Any other exceptions will be treated as :dfn:`failure`. Any other exceptions will be treated as :dfn:`errors`. This
:dfn:`errors`. This helps you identify where the problem is: :dfn:`failures` are helps you identify where the problem is: :dfn:`failures` are caused by incorrect
caused by incorrect results - a 5 where you expected a 6. :dfn:`Errors` are results - a 5 where you expected a 6. :dfn:`Errors` are caused by incorrect
caused by incorrect code - e.g., a :exc:`TypeError` caused by an incorrect code - e.g., a :exc:`TypeError` caused by an incorrect function call.
function call.
The way to run a test case will be described later. For now, note that to The way to run a test case will be described later. For now, note that to
construct an instance of such a test case, we call its constructor without construct an instance of such a test case, we call its constructor without
@ -412,10 +477,10 @@ may treat :exc:`AssertionError` differently.
.. note:: .. note::
Even though :class:`FunctionTestCase` can be used to quickly convert an existing Even though :class:`FunctionTestCase` can be used to quickly convert an
test base over to a :mod:`unittest`\ -based system, this approach is not existing test base over to a :mod:`unittest`\ -based system, this approach is
recommended. Taking the time to set up proper :class:`TestCase` subclasses will not recommended. Taking the time to set up proper :class:`TestCase`
make future test refactorings infinitely easier. subclasses will make future test refactorings infinitely easier.
In some cases, the existing tests may have been written using the :mod:`doctest` In some cases, the existing tests may have been written using the :mod:`doctest`
module. If so, :mod:`doctest` provides a :class:`DocTestSuite` class that can module. If so, :mod:`doctest` provides a :class:`DocTestSuite` class that can
@ -444,7 +509,8 @@ Basic skipping looks like this: ::
def test_nothing(self): def test_nothing(self):
self.fail("shouldn't happen") self.fail("shouldn't happen")
@unittest.skipIf(mylib.__version__ < (1, 3), "not supported in this library version") @unittest.skipIf(mylib.__version__ < (1, 3),
"not supported in this library version")
def test_format(self): def test_format(self):
# Tests that work for only a certain version of the library. # Tests that work for only a certain version of the library.
pass pass
@ -1009,10 +1075,10 @@ Test cases
.. class:: FunctionTestCase(testFunc[, setUp[, tearDown[, description]]]) .. class:: FunctionTestCase(testFunc[, setUp[, tearDown[, description]]])
This class implements the portion of the :class:`TestCase` interface which This class implements the portion of the :class:`TestCase` interface which
allows the test runner to drive the test, but does not provide the methods which allows the test runner to drive the test, but does not provide the methods
test code can use to check and report errors. This is used to create test cases which test code can use to check and report errors. This is used to create
using legacy test code, allowing it to be integrated into a :mod:`unittest`\ test cases using legacy test code, allowing it to be integrated into a
-based test framework. :mod:`unittest`-based test framework.
.. _testsuite-objects: .. _testsuite-objects:
@ -1047,8 +1113,8 @@ Grouping tests
Add all the tests from an iterable of :class:`TestCase` and :class:`TestSuite` Add all the tests from an iterable of :class:`TestCase` and :class:`TestSuite`
instances to this test suite. instances to this test suite.
This is equivalent to iterating over *tests*, calling :meth:`addTest` for each This is equivalent to iterating over *tests*, calling :meth:`addTest` for
element. each element.
:class:`TestSuite` shares the following methods with :class:`TestCase`: :class:`TestSuite` shares the following methods with :class:`TestCase`:
@ -1126,6 +1192,13 @@ Loading and running tests
directly does not play well with this method. Doing so, however, can directly does not play well with this method. Doing so, however, can
be useful when the fixtures are different and defined in subclasses. be useful when the fixtures are different and defined in subclasses.
If a module provides a ``load_tests`` function it will be called to
load the tests. This allows modules to customize test loading.
This is the `load_tests protocol`_.
.. versionchanged:: 2.7
Support for ``load_tests`` added.
.. method:: loadTestsFromName(name[, module]) .. method:: loadTestsFromName(name[, module])
@ -1142,12 +1215,12 @@ Loading and running tests
For example, if you have a module :mod:`SampleTests` containing a For example, if you have a module :mod:`SampleTests` containing a
:class:`TestCase`\ -derived class :class:`SampleTestCase` with three test :class:`TestCase`\ -derived class :class:`SampleTestCase` with three test
methods (:meth:`test_one`, :meth:`test_two`, and :meth:`test_three`), the methods (:meth:`test_one`, :meth:`test_two`, and :meth:`test_three`), the
specifier ``'SampleTests.SampleTestCase'`` would cause this method to return a specifier ``'SampleTests.SampleTestCase'`` would cause this method to
suite which will run all three test methods. Using the specifier return a suite which will run all three test methods. Using the specifier
``'SampleTests.SampleTestCase.test_two'`` would cause it to return a test suite ``'SampleTests.SampleTestCase.test_two'`` would cause it to return a test
which will run only the :meth:`test_two` test method. The specifier can refer suite which will run only the :meth:`test_two` test method. The specifier
to modules and packages which have not been imported; they will be imported as a can refer to modules and packages which have not been imported; they will
side-effect. be imported as a side-effect.
The method optionally resolves *name* relative to the given *module*. The method optionally resolves *name* relative to the given *module*.
@ -1164,6 +1237,31 @@ Loading and running tests
Return a sorted sequence of method names found within *testCaseClass*; Return a sorted sequence of method names found within *testCaseClass*;
this should be a subclass of :class:`TestCase`. this should be a subclass of :class:`TestCase`.
.. method:: discover(start_dir, pattern='test*.py', top_level_dir=None)
Find and return all test modules from the specified start directory,
recursing into subdirectories to find them. Only test files that match
*pattern* will be loaded. (Using shell style pattern matching.)
All test modules must be importable from the top level of the project. If
the start directory is not the top level directory then the top level
directory must be specified separately.
If a test package name (directory with :file:`__init__.py`) matches the
pattern then the package will be checked for a ``load_tests``
function. If this exists then it will be called with *loader*, *tests*,
*pattern*.
If load_tests exists then discovery does *not* recurse into the package,
``load_tests`` is responsible for loading all tests in the package.
The pattern is deliberately not stored as a loader attribute so that
packages can continue discovery themselves. *top_level_dir* is stored so
``load_tests`` does not need to pass this argument in to
``loader.discover()``.
The following attributes of a :class:`TestLoader` can be configured either by The following attributes of a :class:`TestLoader` can be configured either by
subclassing or assignment on an instance: subclassing or assignment on an instance:
@ -1319,8 +1417,8 @@ Loading and running tests
.. method:: addFailure(test, err) .. method:: addFailure(test, err)
Called when the test case *test* signals a failure. *err* is a tuple of the form Called when the test case *test* signals a failure. *err* is a tuple of
returned by :func:`sys.exc_info`: ``(type, value, traceback)``. the form returned by :func:`sys.exc_info`: ``(type, value, traceback)``.
The default implementation appends a tuple ``(test, formatted_err)`` to The default implementation appends a tuple ``(test, formatted_err)`` to
the instance's :attr:`failures` attribute, where *formatted_err* is a the instance's :attr:`failures` attribute, where *formatted_err* is a
@ -1382,7 +1480,7 @@ Loading and running tests
subclasses to provide a custom ``TestResult``. subclasses to provide a custom ``TestResult``.
.. function:: main([module[, defaultTest[, argv[, testRunner[, testLoader[, exit]]]]]]) .. function:: main([module[, defaultTest[, argv[, testRunner[, testLoader[, exit, [verbosity]]]]]]])
A command-line program that runs a set of tests; this is primarily for making A command-line program that runs a set of tests; this is primarily for making
test modules conveniently executable. The simplest use for this function is to test modules conveniently executable. The simplest use for this function is to
@ -1391,6 +1489,12 @@ Loading and running tests
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
You can run tests with more detailed information by passing in the verbosity
argument::
if __name__ == '__main__':
unittest.main(verbosity=2)
The *testRunner* argument can either be a test runner class or an already The *testRunner* argument can either be a test runner class or an already
created instance of it. By default ``main`` calls :func:`sys.exit` with created instance of it. By default ``main`` calls :func:`sys.exit` with
an exit code indicating success or failure of the tests run. an exit code indicating success or failure of the tests run.
@ -1406,4 +1510,69 @@ Loading and running tests
This stores the result of the tests run as the ``result`` attribute. This stores the result of the tests run as the ``result`` attribute.
.. versionchanged:: 2.7 .. versionchanged:: 2.7
The ``exit`` parameter was added. The ``exit`` and ``verbosity`` parameters were added.
load_tests Protocol
###################
Modules or packages can customize how tests are loaded from them during normal
test runs or test discovery by implementing a function called ``load_tests``.
If a test module defines ``load_tests`` it will be called by
:meth:`TestLoader.loadTestsFromModule` with the following arguments::
load_tests(loader, standard_tests, None)
It should return a :class:`TestSuite`.
*loader* is the instance of :class:`TestLoader` doing the loading.
*standard_tests* are the tests that would be loaded by default from the
module. It is common for test modules to only want to add or remove tests
from the standard set of tests.
The third argument is used when loading packages as part of test discovery.
A typical ``load_tests`` function that loads tests from a specific set of
:class:`TestCase` classes may look like::
test_cases = (TestCase1, TestCase2, TestCase3)
def load_tests(loader, tests, pattern):
suite = TestSuite()
for test_class in test_cases:
tests = loader.loadTestsFromTestCase(test_class)
suite.addTests(tests)
return suite
If discovery is started, either from the command line or by calling
:meth:`TestLoader.discover`, with a pattern that matches a package
name then the package :file:`__init__.py` will be checked for ``load_tests``.
.. note::
The default pattern is 'test*.py'. This matches all python files
that start with 'test' but *won't* match any test directories.
A pattern like 'test*' will match test packages as well as
modules.
If the package :file:`__init__.py` defines ``load_tests`` then it will be
called and discovery not continued into the package. ``load_tests``
is called with the following arguments::
load_tests(loader, standard_tests, pattern)
This should return a :class:`TestSuite` representing all the tests
from the package. (``standard_tests`` will only contain tests
collected from :file:`__init__.py`.)
Because the pattern is passed into ``load_tests`` the package is free to
continue (and potentially modify) test discovery. A 'do nothing'
``load_tests`` function for a test package would look like::
def load_tests(loader, standard_tests, pattern):
# top level directory cached on loader instance
this_dir = os.path.dirname(__file__)
package_tests = loader.discover(start_dir=this_dir, pattern=pattern)
standard_tests.addTests(package_tests)
return standard_tests

View File

@ -6,7 +6,9 @@ Still need testing:
TestCase.{assert,fail}* methods (some are tested implicitly) TestCase.{assert,fail}* methods (some are tested implicitly)
""" """
import os
import re import re
import sys
from test import support from test import support
import unittest import unittest
from unittest import TestCase, TestProgram from unittest import TestCase, TestProgram
@ -255,6 +257,30 @@ class Test_TestLoader(TestCase):
reference = [unittest.TestSuite([MyTestCase('test')])] reference = [unittest.TestSuite([MyTestCase('test')])]
self.assertEqual(list(suite), reference) self.assertEqual(list(suite), reference)
# Check that loadTestsFromModule honors (or not) a module
# with a load_tests function.
def test_loadTestsFromModule__load_tests(self):
m = types.ModuleType('m')
class MyTestCase(unittest.TestCase):
def test(self):
pass
m.testcase_1 = MyTestCase
load_tests_args = []
def load_tests(loader, tests, pattern):
load_tests_args.extend((loader, tests, pattern))
return tests
m.load_tests = load_tests
loader = unittest.TestLoader()
suite = loader.loadTestsFromModule(m)
self.assertEquals(load_tests_args, [loader, suite, None])
load_tests_args = []
suite = loader.loadTestsFromModule(m, use_load_tests=False)
self.assertEquals(load_tests_args, [])
################################################################ ################################################################
### /Tests for TestLoader.loadTestsFromModule() ### /Tests for TestLoader.loadTestsFromModule()
@ -3252,19 +3278,30 @@ class Test_TestProgram(TestCase):
runner = FakeRunner() runner = FakeRunner()
try:
oldParseArgs = TestProgram.parseArgs oldParseArgs = TestProgram.parseArgs
def restoreParseArgs():
TestProgram.parseArgs = oldParseArgs
TestProgram.parseArgs = lambda *args: None TestProgram.parseArgs = lambda *args: None
TestProgram.test = test self.addCleanup(restoreParseArgs)
program = TestProgram(testRunner=runner, exit=False) def removeTest():
del TestProgram.test
TestProgram.test = test
self.addCleanup(removeTest)
program = TestProgram(testRunner=runner, exit=False, verbosity=2)
self.assertEqual(program.result, result) self.assertEqual(program.result, result)
self.assertEqual(runner.test, test) self.assertEqual(runner.test, test)
self.assertEqual(program.verbosity, 2)
finally:
TestProgram.parseArgs = oldParseArgs def testTestProgram_testRunnerArgument(self):
del TestProgram.test program = object.__new__(TestProgram)
program.parseArgs = lambda _: None
program.runTests = lambda: None
program.__init__(testRunner=None)
self.assertEqual(program.testRunner, unittest.TextTestRunner)
class FooBar(unittest.TestCase): class FooBar(unittest.TestCase):
@ -3347,6 +3384,277 @@ class Test_TextTestRunner(TestCase):
self.assertEqual(events, expected) self.assertEqual(events, expected)
class TestDiscovery(TestCase):
# Heavily mocked tests so I can avoid hitting the filesystem
def test_get_module_from_path(self):
loader = unittest.TestLoader()
def restore_import():
unittest.__import__ = __import__
unittest.__import__ = lambda *_: None
self.addCleanup(restore_import)
expected_module = object()
def del_module():
del sys.modules['bar.baz']
sys.modules['bar.baz'] = expected_module
self.addCleanup(del_module)
loader._top_level_dir = '/foo'
module = loader._get_module_from_path('/foo/bar/baz.py')
self.assertEqual(module, expected_module)
if not __debug__:
# asserts are off
return
with self.assertRaises(AssertionError):
loader._get_module_from_path('/bar/baz.py')
def test_find_tests(self):
loader = unittest.TestLoader()
original_listdir = os.listdir
def restore_listdir():
os.listdir = original_listdir
original_isfile = os.path.isfile
def restore_isfile():
os.path.isfile = original_isfile
original_isdir = os.path.isdir
def restore_isdir():
os.path.isdir = original_isdir
path_lists = [['test1.py', 'test2.py', 'not_a_test.py', 'test_dir',
'test.foo', 'another_dir'],
['test3.py', 'test4.py', ]]
os.listdir = lambda path: path_lists.pop(0)
self.addCleanup(restore_listdir)
def isdir(path):
return path.endswith('dir')
os.path.isdir = isdir
self.addCleanup(restore_isdir)
def isfile(path):
# another_dir is not a package and so shouldn't be recursed into
return not path.endswith('dir') and not 'another_dir' in path
os.path.isfile = isfile
self.addCleanup(restore_isfile)
loader._get_module_from_path = lambda path: path + ' module'
loader.loadTestsFromModule = lambda module: module + ' tests'
loader._top_level_dir = '/foo'
suite = list(loader._find_tests('/foo', 'test*.py'))
expected = [os.path.join('/foo', name) + ' module tests' for name in
('test1.py', 'test2.py')]
expected.extend([os.path.join('/foo', 'test_dir', name) + ' module tests' for name in
('test3.py', 'test4.py')])
self.assertEqual(suite, expected)
def test_find_tests_with_package(self):
loader = unittest.TestLoader()
original_listdir = os.listdir
def restore_listdir():
os.listdir = original_listdir
original_isfile = os.path.isfile
def restore_isfile():
os.path.isfile = original_isfile
original_isdir = os.path.isdir
def restore_isdir():
os.path.isdir = original_isdir
directories = ['a_directory', 'test_directory', 'test_directory2']
path_lists = [directories, [], [], []]
os.listdir = lambda path: path_lists.pop(0)
self.addCleanup(restore_listdir)
os.path.isdir = lambda path: True
self.addCleanup(restore_isdir)
os.path.isfile = lambda path: os.path.basename(path) not in directories
self.addCleanup(restore_isfile)
class Module(object):
paths = []
load_tests_args = []
def __init__(self, path):
self.path = path
self.paths.append(path)
if os.path.basename(path) == 'test_directory':
def load_tests(loader, tests, pattern):
self.load_tests_args.append((loader, tests, pattern))
return 'load_tests'
self.load_tests = load_tests
def __eq__(self, other):
return self.path == other.path
loader._get_module_from_path = lambda path: Module(path)
def loadTestsFromModule(module, use_load_tests):
if use_load_tests:
raise self.failureException('use_load_tests should be False for packages')
return module.path + ' module tests'
loader.loadTestsFromModule = loadTestsFromModule
loader._top_level_dir = '/foo'
# this time no '.py' on the pattern so that it can match
# a test package
suite = list(loader._find_tests('/foo', 'test*'))
# We should have loaded tests from the test_directory package by calling load_tests
# and directly from the test_directory2 package
self.assertEqual(suite,
['load_tests',
os.path.join('/foo', 'test_directory2') + ' module tests'])
self.assertEqual(Module.paths, [os.path.join('/foo', 'test_directory'),
os.path.join('/foo', 'test_directory2')])
# load_tests should have been called once with loader, tests and pattern
self.assertEqual(Module.load_tests_args,
[(loader, os.path.join('/foo', 'test_directory') + ' module tests',
'test*')])
def test_discover(self):
loader = unittest.TestLoader()
original_isfile = os.path.isfile
def restore_isfile():
os.path.isfile = original_isfile
os.path.isfile = lambda path: False
self.addCleanup(restore_isfile)
full_path = os.path.abspath(os.path.normpath('/foo'))
def clean_path():
if sys.path[-1] == full_path:
sys.path.pop(-1)
self.addCleanup(clean_path)
with self.assertRaises(ImportError):
loader.discover('/foo/bar', top_level_dir='/foo')
self.assertEqual(loader._top_level_dir, full_path)
self.assertIn(full_path, sys.path)
os.path.isfile = lambda path: True
_find_tests_args = []
def _find_tests(start_dir, pattern):
_find_tests_args.append((start_dir, pattern))
return ['tests']
loader._find_tests = _find_tests
loader.suiteClass = str
suite = loader.discover('/foo/bar/baz', 'pattern', '/foo/bar')
top_level_dir = os.path.abspath(os.path.normpath('/foo/bar'))
start_dir = os.path.abspath(os.path.normpath('/foo/bar/baz'))
self.assertEqual(suite, "['tests']")
self.assertEqual(loader._top_level_dir, top_level_dir)
self.assertEqual(_find_tests_args, [(start_dir, 'pattern')])
def test_command_line_handling_parseArgs(self):
# Haha - take that uninstantiable class
program = object.__new__(TestProgram)
args = []
def do_discovery(argv):
args.extend(argv)
program._do_discovery = do_discovery
program.parseArgs(['something', 'discover'])
self.assertEqual(args, [])
program.parseArgs(['something', 'discover', 'foo', 'bar'])
self.assertEqual(args, ['foo', 'bar'])
def test_command_line_handling_do_discovery_too_many_arguments(self):
class Stop(Exception):
pass
def usageExit():
raise Stop
program = object.__new__(TestProgram)
program.usageExit = usageExit
with self.assertRaises(Stop):
# too many args
program._do_discovery(['one', 'two', 'three', 'four'])
def test_command_line_handling_do_discovery_calls_loader(self):
program = object.__new__(TestProgram)
class Loader(object):
args = []
def discover(self, start_dir, pattern, top_level_dir):
self.args.append((start_dir, pattern, top_level_dir))
return 'tests'
program._do_discovery(['-v'], Loader=Loader)
self.assertEqual(program.verbosity, 2)
self.assertEqual(program.test, 'tests')
self.assertEqual(Loader.args, [('.', 'test*.py', None)])
Loader.args = []
program = object.__new__(TestProgram)
program._do_discovery(['--verbose'], Loader=Loader)
self.assertEqual(program.test, 'tests')
self.assertEqual(Loader.args, [('.', 'test*.py', None)])
Loader.args = []
program = object.__new__(TestProgram)
program._do_discovery([], Loader=Loader)
self.assertEqual(program.test, 'tests')
self.assertEqual(Loader.args, [('.', 'test*.py', None)])
Loader.args = []
program = object.__new__(TestProgram)
program._do_discovery(['fish'], Loader=Loader)
self.assertEqual(program.test, 'tests')
self.assertEqual(Loader.args, [('fish', 'test*.py', None)])
Loader.args = []
program = object.__new__(TestProgram)
program._do_discovery(['fish', 'eggs'], Loader=Loader)
self.assertEqual(program.test, 'tests')
self.assertEqual(Loader.args, [('fish', 'eggs', None)])
Loader.args = []
program = object.__new__(TestProgram)
program._do_discovery(['fish', 'eggs', 'ham'], Loader=Loader)
self.assertEqual(program.test, 'tests')
self.assertEqual(Loader.args, [('fish', 'eggs', 'ham')])
Loader.args = []
program = object.__new__(TestProgram)
program._do_discovery(['-s', 'fish'], Loader=Loader)
self.assertEqual(program.test, 'tests')
self.assertEqual(Loader.args, [('fish', 'test*.py', None)])
Loader.args = []
program = object.__new__(TestProgram)
program._do_discovery(['-t', 'fish'], Loader=Loader)
self.assertEqual(program.test, 'tests')
self.assertEqual(Loader.args, [('.', 'test*.py', 'fish')])
Loader.args = []
program = object.__new__(TestProgram)
program._do_discovery(['-p', 'fish'], Loader=Loader)
self.assertEqual(program.test, 'tests')
self.assertEqual(Loader.args, [('.', 'fish', None)])
Loader.args = []
program = object.__new__(TestProgram)
program._do_discovery(['-p', 'eggs', '-s', 'fish', '-v'], Loader=Loader)
self.assertEqual(program.test, 'tests')
self.assertEqual(Loader.args, [('fish', 'eggs', None)])
self.assertEqual(program.verbosity, 2)
###################################################################### ######################################################################
## Main ## Main
###################################################################### ######################################################################
@ -3355,7 +3663,7 @@ def test_main():
support.run_unittest(Test_TestCase, Test_TestLoader, support.run_unittest(Test_TestCase, Test_TestLoader,
Test_TestSuite, Test_TestResult, Test_FunctionTestCase, Test_TestSuite, Test_TestResult, Test_FunctionTestCase,
Test_TestSkipping, Test_Assertions, TestLongMessage, Test_TestSkipping, Test_Assertions, TestLongMessage,
Test_TestProgram, TestCleanUp) Test_TestProgram, TestCleanUp, TestDiscovery)
if __name__ == "__main__": if __name__ == "__main__":
test_main() test_main()

View File

@ -56,6 +56,9 @@ import traceback
import types import types
import warnings import warnings
from fnmatch import fnmatch
############################################################################## ##############################################################################
# Exported classes and functions # Exported classes and functions
############################################################################## ##############################################################################
@ -1228,6 +1231,7 @@ class TestLoader(object):
testMethodPrefix = 'test' testMethodPrefix = 'test'
sortTestMethodsUsing = staticmethod(three_way_cmp) sortTestMethodsUsing = staticmethod(three_way_cmp)
suiteClass = TestSuite suiteClass = TestSuite
_top_level_dir = None
def loadTestsFromTestCase(self, testCaseClass): def loadTestsFromTestCase(self, testCaseClass):
"""Return a suite of all tests cases contained in testCaseClass""" """Return a suite of all tests cases contained in testCaseClass"""
@ -1240,13 +1244,17 @@ class TestLoader(object):
suite = self.suiteClass(map(testCaseClass, testCaseNames)) suite = self.suiteClass(map(testCaseClass, testCaseNames))
return suite return suite
def loadTestsFromModule(self, module): def loadTestsFromModule(self, module, use_load_tests=True):
"""Return a suite of all tests cases contained in the given module""" """Return a suite of all tests cases contained in the given module"""
tests = [] tests = []
for name in dir(module): for name in dir(module):
obj = getattr(module, name) obj = getattr(module, name)
if isinstance(obj, type) and issubclass(obj, TestCase): if isinstance(obj, type) and issubclass(obj, TestCase):
tests.append(self.loadTestsFromTestCase(obj)) tests.append(self.loadTestsFromTestCase(obj))
load_tests = getattr(module, 'load_tests', None)
if use_load_tests and load_tests is not None:
return load_tests(self, tests, None)
return self.suiteClass(tests) return self.suiteClass(tests)
def loadTestsFromName(self, name, module=None): def loadTestsFromName(self, name, module=None):
@ -1320,7 +1328,97 @@ class TestLoader(object):
testFnNames.sort(key=CmpToKey(self.sortTestMethodsUsing)) testFnNames.sort(key=CmpToKey(self.sortTestMethodsUsing))
return testFnNames return testFnNames
def discover(self, start_dir, pattern='test*.py', top_level_dir=None):
"""Find and return all test modules from the specified start
directory, recursing into subdirectories to find them. Only test files
that match the pattern will be loaded. (Using shell style pattern
matching.)
All test modules must be importable from the top level of the project.
If the start directory is not the top level directory then the top
level directory must be specified separately.
If a test package name (directory with '__init__.py') matches the
pattern then the package will be checked for a 'load_tests' function. If
this exists then it will be called with loader, tests, pattern.
If load_tests exists then discovery does *not* recurse into the package,
load_tests is responsible for loading all tests in the package.
The pattern is deliberately not stored as a loader attribute so that
packages can continue discovery themselves. top_level_dir is stored so
load_tests does not need to pass this argument in to loader.discover().
"""
if top_level_dir is None and self._top_level_dir is not None:
# make top_level_dir optional if called from load_tests in a package
top_level_dir = self._top_level_dir
elif top_level_dir is None:
top_level_dir = start_dir
top_level_dir = os.path.abspath(os.path.normpath(top_level_dir))
start_dir = os.path.abspath(os.path.normpath(start_dir))
if not top_level_dir in sys.path:
# all test modules must be importable from the top level directory
sys.path.append(top_level_dir)
self._top_level_dir = top_level_dir
if start_dir != top_level_dir and not os.path.isfile(os.path.join(start_dir, '__init__.py')):
# what about __init__.pyc or pyo (etc)
raise ImportError('Start directory is not importable: %r' % start_dir)
tests = list(self._find_tests(start_dir, pattern))
return self.suiteClass(tests)
def _get_module_from_path(self, path):
"""Load a module from a path relative to the top-level directory
of a project. Used by discovery."""
path = os.path.splitext(os.path.normpath(path))[0]
relpath = os.path.relpath(path, self._top_level_dir)
assert not os.path.isabs(relpath), "Path must be within the project"
assert not relpath.startswith('..'), "Path must be within the project"
name = relpath.replace(os.path.sep, '.')
__import__(name)
return sys.modules[name]
def _find_tests(self, start_dir, pattern):
"""Used by discovery. Yields test suites it loads."""
paths = os.listdir(start_dir)
for path in paths:
full_path = os.path.join(start_dir, path)
# what about __init__.pyc or pyo (etc)
# we would need to avoid loading the same tests multiple times
# from '.py', '.pyc' *and* '.pyo'
if os.path.isfile(full_path) and path.lower().endswith('.py'):
if fnmatch(path, pattern):
# if the test file matches, load it
module = self._get_module_from_path(full_path)
yield self.loadTestsFromModule(module)
elif os.path.isdir(full_path):
if not os.path.isfile(os.path.join(full_path, '__init__.py')):
continue
load_tests = None
tests = None
if fnmatch(path, pattern):
# only check load_tests if the package directory itself matches the filter
package = self._get_module_from_path(full_path)
load_tests = getattr(package, 'load_tests', None)
tests = self.loadTestsFromModule(package, use_load_tests=False)
if load_tests is None:
if tests is not None:
# tests loaded from package file
yield tests
# recurse into the package
for test in self._find_tests(full_path, pattern):
yield test
else:
yield load_tests(self, tests, pattern)
defaultTestLoader = TestLoader() defaultTestLoader = TestLoader()
@ -1525,11 +1623,37 @@ class TextTestRunner(object):
# Facilities for running tests from the command line # Facilities for running tests from the command line
############################################################################## ##############################################################################
class TestProgram(object): USAGE_AS_MAIN = """\
"""A command-line program that runs a set of tests; this is primarily Usage: %(progName)s [options] [tests]
for making test modules conveniently executable.
Options:
-h, --help Show this message
-v, --verbose Verbose output
-q, --quiet Minimal output
Examples:
%(progName)s test_module - run tests from test_module
%(progName)s test_module.TestClass - run tests from
test_module.TestClass
%(progName)s test_module.TestClass.test_method - run specified test method
[tests] can be a list of any number of test modules, classes and test
methods.
Alternative Usage: %(progName)s discover [options]
Options:
-v, --verbose Verbose output
-s directory Directory to start discovery ('.' default)
-p pattern Pattern to match test files ('test*.py' default)
-t directory Top level directory of project (default to
start directory)
For test discovery all test modules must be importable from the top
level directory of the project.
""" """
USAGE = """\
USAGE_FROM_MODULE = """\
Usage: %(progName)s [options] [test] [...] Usage: %(progName)s [options] [test] [...]
Options: Options:
@ -1544,9 +1668,24 @@ Examples:
%(progName)s MyTestCase - run all 'test*' test methods %(progName)s MyTestCase - run all 'test*' test methods
in MyTestCase in MyTestCase
""" """
if __name__ == '__main__':
USAGE = USAGE_AS_MAIN
else:
USAGE = USAGE_FROM_MODULE
class TestProgram(object):
"""A command-line program that runs a set of tests; this is primarily
for making test modules conveniently executable.
"""
USAGE = USAGE
def __init__(self, module='__main__', defaultTest=None, def __init__(self, module='__main__', defaultTest=None,
argv=None, testRunner=TextTestRunner, argv=None, testRunner=None,
testLoader=defaultTestLoader, exit=True): testLoader=defaultTestLoader, exit=True,
verbosity=1):
if testRunner is None:
testRunner = TextTestRunner
if isinstance(module, str): if isinstance(module, str):
self.module = __import__(module) self.module = __import__(module)
for part in module.split('.')[1:]: for part in module.split('.')[1:]:
@ -1557,7 +1696,7 @@ Examples:
argv = sys.argv argv = sys.argv
self.exit = exit self.exit = exit
self.verbosity = 1 self.verbosity = verbosity
self.defaultTest = defaultTest self.defaultTest = defaultTest
self.testRunner = testRunner self.testRunner = testRunner
self.testLoader = testLoader self.testLoader = testLoader
@ -1572,6 +1711,10 @@ Examples:
sys.exit(2) sys.exit(2)
def parseArgs(self, argv): def parseArgs(self, argv):
if len(argv) > 1 and argv[1].lower() == 'discover':
self._do_discovery(argv[2:])
return
import getopt import getopt
long_opts = ['help','verbose','quiet'] long_opts = ['help','verbose','quiet']
try: try:
@ -1588,6 +1731,9 @@ Examples:
return return
if len(args) > 0: if len(args) > 0:
self.testNames = args self.testNames = args
if __name__ == '__main__':
# to support python -m unittest ...
self.module = None
else: else:
self.testNames = (self.defaultTest,) self.testNames = (self.defaultTest,)
self.createTests() self.createTests()
@ -1598,6 +1744,36 @@ Examples:
self.test = self.testLoader.loadTestsFromNames(self.testNames, self.test = self.testLoader.loadTestsFromNames(self.testNames,
self.module) self.module)
def _do_discovery(self, argv, Loader=TestLoader):
# handle command line args for test discovery
import optparse
parser = optparse.OptionParser()
parser.add_option('-v', '--verbose', dest='verbose', default=False,
help='Verbose output', action='store_true')
parser.add_option('-s', '--start-directory', dest='start', default='.',
help="Directory to start discovery ('.' default)")
parser.add_option('-p', '--pattern', dest='pattern', default='test*.py',
help="Pattern to match tests ('test*.py' default)")
parser.add_option('-t', '--top-level-directory', dest='top', default=None,
help='Top level directory of project (defaults to start directory)')
options, args = parser.parse_args(argv)
if len(args) > 3:
self.usageExit()
for name, value in zip(('start', 'pattern', 'top'), args):
setattr(options, name, value)
if options.verbose:
self.verbosity = 2
start_dir = options.start
pattern = options.pattern
top_level_dir = options.top
loader = Loader()
self.test = loader.discover(start_dir, pattern, top_level_dir)
def runTests(self): def runTests(self):
if isinstance(self.testRunner, type): if isinstance(self.testRunner, type):
try: try:
@ -1620,4 +1796,5 @@ main = TestProgram
############################################################################## ##############################################################################
if __name__ == "__main__": if __name__ == "__main__":
sys.modules['unittest'] = sys.modules['__main__']
main(module=None) main(module=None)