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.
`Simple Smalltalk Testing: With Patterns <http://www.XProgramming.com/testfram.htm>`_
Kent Beck's original paper on testing frameworks using the pattern shared by
:mod:`unittest`.
Kent Beck's original paper on testing frameworks using the pattern shared
by :mod:`unittest`.
`Nose <http://code.google.com/p/python-nose/>`_ and `py.test <http://pytest.org>`_
Third-party unittest frameworks with a lighter-weight syntax
for writing tests. For example, ``assert func(10) == 42``.
Third-party unittest frameworks with a lighter-weight syntax for writing
tests. For example, ``assert func(10) == 42``.
`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:
@ -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
documentation explores the full feature set from first principles.
.. _organizing-tests:
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')
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
test fails, an exception will be raised, and :mod:`unittest` will identify the
test case as a :dfn:`failure`. Any other exceptions will be treated as
:dfn:`errors`. This helps you identify where the problem is: :dfn:`failures` are
caused by incorrect results - a 5 where you expected a 6. :dfn:`Errors` are
caused by incorrect code - e.g., a :exc:`TypeError` caused by an incorrect
function call.
methods provided by the :class:`TestCase` base class. If the test fails, an
exception will be raised, and :mod:`unittest` will identify the test case as a
:dfn:`failure`. Any other exceptions will be treated as :dfn:`errors`. This
helps you identify where the problem is: :dfn:`failures` are caused by incorrect
results - a 5 where you expected a 6. :dfn:`Errors` are caused by incorrect
code - e.g., a :exc:`TypeError` caused by an incorrect function call.
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
@ -412,10 +477,10 @@ may treat :exc:`AssertionError` differently.
.. note::
Even though :class:`FunctionTestCase` can be used to quickly convert an existing
test base over to a :mod:`unittest`\ -based system, this approach is not
recommended. Taking the time to set up proper :class:`TestCase` subclasses will
make future test refactorings infinitely easier.
Even though :class:`FunctionTestCase` can be used to quickly convert an
existing test base over to a :mod:`unittest`\ -based system, this approach is
not recommended. Taking the time to set up proper :class:`TestCase`
subclasses will make future test refactorings infinitely easier.
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
@ -444,7 +509,8 @@ Basic skipping looks like this: ::
def test_nothing(self):
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):
# Tests that work for only a certain version of the library.
pass
@ -1009,10 +1075,10 @@ Test cases
.. class:: FunctionTestCase(testFunc[, setUp[, tearDown[, description]]])
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
test code can use to check and report errors. This is used to create test cases
using legacy test code, allowing it to be integrated into a :mod:`unittest`\
-based test framework.
allows the test runner to drive the test, but does not provide the methods
which test code can use to check and report errors. This is used to create
test cases using legacy test code, allowing it to be integrated into a
:mod:`unittest`-based test framework.
.. _testsuite-objects:
@ -1047,8 +1113,8 @@ Grouping tests
Add all the tests from an iterable of :class:`TestCase` and :class:`TestSuite`
instances to this test suite.
This is equivalent to iterating over *tests*, calling :meth:`addTest` for each
element.
This is equivalent to iterating over *tests*, calling :meth:`addTest` for
each element.
: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
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])
@ -1142,12 +1215,12 @@ Loading and running tests
For example, if you have a module :mod:`SampleTests` containing a
:class:`TestCase`\ -derived class :class:`SampleTestCase` with three test
methods (:meth:`test_one`, :meth:`test_two`, and :meth:`test_three`), the
specifier ``'SampleTests.SampleTestCase'`` would cause this method to 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
which will run only the :meth:`test_two` test method. The specifier can refer
to modules and packages which have not been imported; they will be imported as a
side-effect.
specifier ``'SampleTests.SampleTestCase'`` would cause this method to
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 which will run only the :meth:`test_two` test method. The specifier
can refer to modules and packages which have not been imported; they will
be imported as a side-effect.
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*;
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
subclassing or assignment on an instance:
@ -1319,8 +1417,8 @@ Loading and running tests
.. method:: addFailure(test, err)
Called when the test case *test* signals a failure. *err* is a tuple of the form
returned by :func:`sys.exc_info`: ``(type, value, traceback)``.
Called when the test case *test* signals a failure. *err* is a tuple of
the form returned by :func:`sys.exc_info`: ``(type, value, traceback)``.
The default implementation appends a tuple ``(test, formatted_err)`` to
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``.
.. 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
test modules conveniently executable. The simplest use for this function is to
@ -1391,6 +1489,12 @@ Loading and running tests
if __name__ == '__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
created instance of it. By default ``main`` calls :func:`sys.exit` with
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.
.. 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)
"""
import os
import re
import sys
from test import support
import unittest
from unittest import TestCase, TestProgram
@ -255,6 +257,30 @@ class Test_TestLoader(TestCase):
reference = [unittest.TestSuite([MyTestCase('test')])]
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()
@ -3252,19 +3278,30 @@ class Test_TestProgram(TestCase):
runner = FakeRunner()
try:
oldParseArgs = TestProgram.parseArgs
TestProgram.parseArgs = lambda *args: None
TestProgram.test = test
program = TestProgram(testRunner=runner, exit=False)
self.assertEqual(program.result, result)
self.assertEqual(runner.test, test)
finally:
oldParseArgs = TestProgram.parseArgs
def restoreParseArgs():
TestProgram.parseArgs = oldParseArgs
TestProgram.parseArgs = lambda *args: None
self.addCleanup(restoreParseArgs)
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(runner.test, test)
self.assertEqual(program.verbosity, 2)
def testTestProgram_testRunnerArgument(self):
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):
@ -3347,6 +3384,277 @@ class Test_TextTestRunner(TestCase):
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
######################################################################
@ -3355,7 +3663,7 @@ def test_main():
support.run_unittest(Test_TestCase, Test_TestLoader,
Test_TestSuite, Test_TestResult, Test_FunctionTestCase,
Test_TestSkipping, Test_Assertions, TestLongMessage,
Test_TestProgram, TestCleanUp)
Test_TestProgram, TestCleanUp, TestDiscovery)
if __name__ == "__main__":
test_main()

View File

@ -56,6 +56,9 @@ import traceback
import types
import warnings
from fnmatch import fnmatch
##############################################################################
# Exported classes and functions
##############################################################################
@ -1228,6 +1231,7 @@ class TestLoader(object):
testMethodPrefix = 'test'
sortTestMethodsUsing = staticmethod(three_way_cmp)
suiteClass = TestSuite
_top_level_dir = None
def loadTestsFromTestCase(self, testCaseClass):
"""Return a suite of all tests cases contained in testCaseClass"""
@ -1240,13 +1244,17 @@ class TestLoader(object):
suite = self.suiteClass(map(testCaseClass, testCaseNames))
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"""
tests = []
for name in dir(module):
obj = getattr(module, name)
if isinstance(obj, type) and issubclass(obj, TestCase):
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)
def loadTestsFromName(self, name, module=None):
@ -1320,7 +1328,97 @@ class TestLoader(object):
testFnNames.sort(key=CmpToKey(self.sortTestMethodsUsing))
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()
@ -1525,11 +1623,37 @@ class TextTestRunner(object):
# Facilities for running tests from the command line
##############################################################################
class TestProgram(object):
"""A command-line program that runs a set of tests; this is primarily
for making test modules conveniently executable.
"""
USAGE = """\
USAGE_AS_MAIN = """\
Usage: %(progName)s [options] [tests]
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_FROM_MODULE = """\
Usage: %(progName)s [options] [test] [...]
Options:
@ -1544,9 +1668,24 @@ Examples:
%(progName)s MyTestCase - run all 'test*' test methods
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,
argv=None, testRunner=TextTestRunner,
testLoader=defaultTestLoader, exit=True):
argv=None, testRunner=None,
testLoader=defaultTestLoader, exit=True,
verbosity=1):
if testRunner is None:
testRunner = TextTestRunner
if isinstance(module, str):
self.module = __import__(module)
for part in module.split('.')[1:]:
@ -1557,7 +1696,7 @@ Examples:
argv = sys.argv
self.exit = exit
self.verbosity = 1
self.verbosity = verbosity
self.defaultTest = defaultTest
self.testRunner = testRunner
self.testLoader = testLoader
@ -1572,6 +1711,10 @@ Examples:
sys.exit(2)
def parseArgs(self, argv):
if len(argv) > 1 and argv[1].lower() == 'discover':
self._do_discovery(argv[2:])
return
import getopt
long_opts = ['help','verbose','quiet']
try:
@ -1588,6 +1731,9 @@ Examples:
return
if len(args) > 0:
self.testNames = args
if __name__ == '__main__':
# to support python -m unittest ...
self.module = None
else:
self.testNames = (self.defaultTest,)
self.createTests()
@ -1598,6 +1744,36 @@ Examples:
self.test = self.testLoader.loadTestsFromNames(self.testNames,
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):
if isinstance(self.testRunner, type):
try:
@ -1620,4 +1796,5 @@ main = TestProgram
##############################################################################
if __name__ == "__main__":
sys.modules['unittest'] = sys.modules['__main__']
main(module=None)