diff options
-rw-r--r-- | Lib/test/test_support.py | 16 | ||||
-rw-r--r-- | Lib/unittest.py | 484 |
2 files changed, 244 insertions, 256 deletions
diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index ba34ac8..426f7aa 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -102,12 +102,9 @@ def check_syntax(statement): import unittest -class BasicTestRunner(unittest.VerboseTextTestRunner): - def __init__(self, stream=sys.stderr): - unittest.VerboseTextTestRunner.__init__(self, stream, descriptions=0) - +class BasicTestRunner: def run(self, test): - result = unittest._VerboseTextTestResult(self.stream, descriptions=0) + result = unittest.TestResult() test(result) return result @@ -115,13 +112,12 @@ class BasicTestRunner(unittest.VerboseTextTestRunner): def run_unittest(testclass): """Run tests from a unittest.TestCase-derived class.""" if verbose: - f = sys.stdout + runner = unittest.TextTestRunner(sys.stdout, descriptions=0) else: - import StringIO - f = StringIO.StringIO() + runner = BasicTestRunner() suite = unittest.makeSuite(testclass) - result = BasicTestRunner(stream=f).run(suite) - if result.errors or result.failures: + result = runner.run(suite) + if not result.wasSuccessful(): raise TestFailed("errors occurred in %s.%s" % (testclass.__module__, testclass.__name__)) diff --git a/Lib/unittest.py b/Lib/unittest.py index b3eec18..850c38e 100644 --- a/Lib/unittest.py +++ b/Lib/unittest.py @@ -1,17 +1,32 @@ #!/usr/bin/env python -""" +''' Python unit testing framework, based on Erich Gamma's JUnit and Kent Beck's Smalltalk testing framework. -Further information is available in the bundled documentation, and from - - http://pyunit.sourceforge.net/ - This module contains the core framework classes that form the basis of specific test cases and suites (TestCase, TestSuite etc.), and also a text-based utility class for running the tests and reporting the results (TextTestRunner). +Simple usage: + + import unittest + + class IntegerArithmenticTestCase(unittest.TestCase): + def testAdd(self): ## test method names begin 'test*' + self.assertEquals((1 + 2), 3) + self.assertEquals(0 + 1, 1) + def testMultiply(self); + self.assertEquals((0 * 10), 0) + self.assertEquals((5 * 8), 40) + + if __name__ == '__main__': + unittest.main() + +Further information is available in the bundled documentation, and from + + http://pyunit.sourceforge.net/ + Copyright (c) 1999, 2000, 2001 Steve Purcell This module is free software, and you may redistribute it and/or modify it under the same terms as Python itself, so long as this copyright message @@ -27,9 +42,10 @@ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. -""" +''' -__author__ = "Steve Purcell (stephen_purcell@yahoo.com)" +__author__ = "Steve Purcell" +__email__ = "stephen_purcell at yahoo dot com" __version__ = "$Revision$"[11:-2] import time @@ -37,15 +53,7 @@ import sys import traceback import string import os - -############################################################################## -# A platform-specific concession to help the code work for JPython users -############################################################################## - -plat = string.lower(sys.platform) -_isJPython = string.find(plat, 'java') >= 0 or string.find(plat, 'jdk') >= 0 -del plat - +import types ############################################################################## # Test framework core @@ -84,6 +92,10 @@ class TestResult: "Called when a failure has occurred" self.failures.append((test, err)) + def addSuccess(self, test): + "Called when a test has completed successfully" + pass + def wasSuccessful(self): "Tells whether or not this result was a success" return len(self.failures) == len(self.errors) == 0 @@ -101,17 +113,23 @@ class TestResult: class TestCase: """A class whose instances are single test cases. - Test authors should subclass TestCase for their own tests. Construction - and deconstruction of the test's environment ('fixture') can be - implemented by overriding the 'setUp' and 'tearDown' methods respectively. - By default, the test code itself should be placed in a method named 'runTest'. - + If the fixture may be used for many test cases, create as many test methods as are needed. When instantiating such a TestCase subclass, specify in the constructor arguments the name of the test method that the instance is to execute. + + Test authors should subclass TestCase for their own tests. Construction + and deconstruction of the test's environment ('fixture') can be + implemented by overriding the 'setUp' and 'tearDown' methods respectively. + + If it is necessary to override the __init__ method, the base class + __init__ method must always be called. It is important that subclasses + should not change the signature of their __init__ method, since instances + of the classes are instantiated automatically by parts of the framework + in order to be run. """ def __init__(self, methodName='runTest'): """Create an instance of the class that will use the named test @@ -119,7 +137,9 @@ class TestCase: not have a method with the specified name. """ try: - self.__testMethod = getattr(self,methodName) + self.__testMethodName = methodName + testMethod = getattr(self, methodName) + self.__testMethodDoc = testMethod.__doc__ except AttributeError: raise ValueError, "no such test method in %s: %s" % \ (self.__class__, methodName) @@ -145,18 +165,18 @@ class TestCase: The default implementation of this method returns the first line of the specified test method's docstring. """ - doc = self.__testMethod.__doc__ + doc = self.__testMethodDoc return doc and string.strip(string.split(doc, "\n")[0]) or None def id(self): - return "%s.%s" % (self.__class__, self.__testMethod.__name__) + return "%s.%s" % (self.__class__, self.__testMethodName) def __str__(self): - return "%s (%s)" % (self.__testMethod.__name__, self.__class__) + return "%s (%s)" % (self.__testMethodName, self.__class__) def __repr__(self): return "<%s testMethod=%s>" % \ - (self.__class__, self.__testMethod.__name__) + (self.__class__, self.__testMethodName) def run(self, result=None): return self(result) @@ -164,6 +184,7 @@ class TestCase: def __call__(self, result=None): if result is None: result = self.defaultTestResult() result.startTest(self) + testMethod = getattr(self, self.__testMethodName) try: try: self.setUp() @@ -171,8 +192,10 @@ class TestCase: result.addError(self,self.__exc_info()) return + ok = 0 try: - self.__testMethod() + testMethod() + ok = 1 except AssertionError, e: result.addFailure(self,self.__exc_info()) except: @@ -182,12 +205,15 @@ class TestCase: self.tearDown() except: result.addError(self,self.__exc_info()) + ok = 0 + if ok: result.addSuccess(self) finally: result.stopTest(self) def debug(self): + """Run the test without collecting errors in a TestResult""" self.setUp() - self.__testMethod() + getattr(self, self.__testMethodName)() self.tearDown() def assert_(self, expr, msg=None): @@ -220,10 +246,26 @@ class TestCase: else: excName = str(excClass) raise AssertionError, excName + def assertEquals(self, first, second, msg=None): + """Assert that the two objects are equal as determined by the '==' + operator. + """ + self.assert_((first == second), msg or '%s != %s' % (first, second)) + + def assertNotEquals(self, first, second, msg=None): + """Assert that the two objects are unequal as determined by the '!=' + operator. + """ + self.assert_((first != second), msg or '%s == %s' % (first, second)) + + assertEqual = assertEquals + + assertNotEqual = assertNotEquals + def fail(self, msg=None): """Fail immediately, with the given message.""" raise AssertionError, msg - + def __exc_info(self): """Return a version of sys.exc_info() with the traceback frame minimised; usually the top level of the traceback frame is not @@ -278,8 +320,8 @@ class TestSuite: return result def debug(self): + """Run the tests without collecting errors in a TestResult""" for test in self._tests: test.debug() - class FunctionTestCase(TestCase): @@ -327,84 +369,100 @@ class FunctionTestCase(TestCase): ############################################################################## -# Convenience functions +# Locating and loading tests ############################################################################## -def getTestCaseNames(testCaseClass, prefix, sortUsing=cmp): - """Extracts all the names of functions in the given test case class - and its base classes that start with the given prefix. This is used - by makeSuite(). - """ - testFnNames = filter(lambda n,p=prefix: n[:len(p)] == p, - dir(testCaseClass)) - for baseclass in testCaseClass.__bases__: - testFnNames = testFnNames + \ - getTestCaseNames(baseclass, prefix, sortUsing=None) - if sortUsing: - testFnNames.sort(sortUsing) - return testFnNames - - -def makeSuite(testCaseClass, prefix='test', sortUsing=cmp): - """Returns a TestSuite instance built from all of the test functions - in the given test case class whose names begin with the given - prefix. The cases are sorted by their function names - using the supplied comparison function, which defaults to 'cmp'. +class TestLoader: + """This class is responsible for loading tests according to various + criteria and returning them wrapped in a Test + + It can load all tests within a given, module """ - cases = map(testCaseClass, - getTestCaseNames(testCaseClass, prefix, sortUsing)) - return TestSuite(cases) + testMethodPrefix = 'test' + sortTestMethodsUsing = cmp + suiteClass = TestSuite + def loadTestsFromTestCase(self, testCaseClass): + return self.suiteClass(map(testCaseClass, + self.getTestCaseNames(testCaseClass))) -def createTestInstance(name, module=None): - """Finds tests by their name, optionally only within the given module. + def loadTestsFromModule(self, module): + tests = [] + for name in dir(module): + obj = getattr(module, name) + if type(obj) == types.ClassType and issubclass(obj, TestCase): + tests.append(self.loadTestsFromTestCase(obj)) + return self.suiteClass(tests) + + def loadTestsFromName(self, name, module=None): + parts = string.split(name, '.') + if module is None: + if not parts: + raise ValueError, "incomplete test name: %s" % name + else: + module = __import__(parts) + parts = parts[1:] + obj = module + for part in parts: + obj = getattr(obj, part) + + if type(obj) == types.ModuleType: + return self.loadTestsFromModule(obj) + elif type(obj) == types.ClassType and issubclass(obj, TestCase): + return self.loadTestsFromTestCase(obj) + elif type(obj) == types.UnboundMethodType: + return obj.im_class(obj.__name__) + elif callable(obj): + test = obj() + if not isinstance(test, TestCase) and \ + not isinstance(test, TestSuite): + raise ValueError, \ + "calling %s returned %s, not a test" % obj,test + return test + else: + raise ValueError, "don't know how to make test from: %s" % obj - Return the newly-constructed test, ready to run. If the name contains a ':' - then the portion of the name after the colon is used to find a specific - test case within the test case class named before the colon. + def loadTestsFromNames(self, names, module=None): + suites = [] + for name in names: + suites.append(self.loadTestsFromName(name, module)) + return self.suiteClass(suites) - Examples: - findTest('examples.listtests.suite') - -- returns result of calling 'suite' - findTest('examples.listtests.ListTestCase:checkAppend') - -- returns result of calling ListTestCase('checkAppend') - findTest('examples.listtests.ListTestCase:check-') - -- returns result of calling makeSuite(ListTestCase, prefix="check") - """ - - spec = string.split(name, ':') - if len(spec) > 2: raise ValueError, "illegal test name: %s" % name - if len(spec) == 1: - testName = spec[0] - caseName = None - else: - testName, caseName = spec - parts = string.split(testName, '.') - if module is None: - if len(parts) < 2: - raise ValueError, "incomplete test name: %s" % name - constructor = __import__(string.join(parts[:-1],'.')) - parts = parts[1:] - else: - constructor = module - for part in parts: - constructor = getattr(constructor, part) - if not callable(constructor): - raise ValueError, "%s is not a callable object" % constructor - if caseName: - if caseName[-1] == '-': - prefix = caseName[:-1] - if not prefix: - raise ValueError, "prefix too short: %s" % name - test = makeSuite(constructor, prefix=prefix) - else: - test = constructor(caseName) - else: - test = constructor() - if not hasattr(test,"countTestCases"): - raise TypeError, \ - "object %s found with spec %s is not a test" % (test, name) - return test + def getTestCaseNames(self, testCaseClass): + testFnNames = filter(lambda n,p=self.testMethodPrefix: n[:len(p)] == p, + dir(testCaseClass)) + for baseclass in testCaseClass.__bases__: + for testFnName in self.getTestCaseNames(baseclass): + if testFnName not in testFnNames: # handle overridden methods + testFnNames.append(testFnName) + if self.sortTestMethodsUsing: + testFnNames.sort(self.sortTestMethodsUsing) + return testFnNames + + + +defaultTestLoader = TestLoader() + + +############################################################################## +# Patches for old functions: these functions should be considered obsolete +############################################################################## + +def _makeLoader(prefix, sortUsing, suiteClass=None): + loader = TestLoader() + loader.sortTestMethodsUsing = sortUsing + loader.testMethodPrefix = prefix + if suiteClass: loader.suiteClass = suiteClass + return loader + +def getTestCaseNames(testCaseClass, prefix, sortUsing=cmp): + return _makeLoader(prefix, sortUsing).getTestCaseNames(testCaseClass) + +def makeSuite(testCaseClass, prefix='test', sortUsing=cmp, suiteClass=TestSuite): + return _makeLoader(prefix, sortUsing, suiteClass).loadTestsFromTestCase(testCaseClass) + +def findTestCases(module, prefix='test', sortUsing=cmp, suiteClass=TestSuite): + return _makeLoader(prefix, sortUsing, suiteClass).loadTestsFromModule(module) ############################################################################## @@ -415,181 +473,107 @@ class _WritelnDecorator: """Used to decorate file-like objects with a handy 'writeln' method""" def __init__(self,stream): self.stream = stream - if _isJPython: - import java.lang.System - self.linesep = java.lang.System.getProperty("line.separator") - else: - self.linesep = os.linesep def __getattr__(self, attr): return getattr(self.stream,attr) def writeln(self, *args): if args: apply(self.write, args) - self.write(self.linesep) - - -class _JUnitTextTestResult(TestResult): - """A test result class that can print formatted text results to a stream. - - Used by JUnitTextTestRunner. - """ - def __init__(self, stream): - self.stream = stream - TestResult.__init__(self) - - def addError(self, test, error): - TestResult.addError(self,test,error) - self.stream.write('E') - self.stream.flush() - if error[0] is KeyboardInterrupt: - self.shouldStop = 1 + self.write('\n') # text-mode streams translate to \r\n if needed - def addFailure(self, test, error): - TestResult.addFailure(self,test,error) - self.stream.write('F') - self.stream.flush() - - def startTest(self, test): - TestResult.startTest(self,test) - self.stream.write('.') - self.stream.flush() - - def printNumberedErrors(self,errFlavour,errors): - if not errors: return - if len(errors) == 1: - self.stream.writeln("There was 1 %s:" % errFlavour) - else: - self.stream.writeln("There were %i %ss:" % - (len(errors), errFlavour)) - i = 1 - for test,error in errors: - errString = string.join(apply(traceback.format_exception,error),"") - self.stream.writeln("%i) %s" % (i, test)) - self.stream.writeln(errString) - i = i + 1 - - def printErrors(self): - self.printNumberedErrors("error",self.errors) - - def printFailures(self): - self.printNumberedErrors("failure",self.failures) - - def printHeader(self): - self.stream.writeln() - if self.wasSuccessful(): - self.stream.writeln("OK (%i tests)" % self.testsRun) - else: - self.stream.writeln("!!!FAILURES!!!") - self.stream.writeln("Test Results") - self.stream.writeln() - self.stream.writeln("Run: %i ; Failures: %i ; Errors: %i" % - (self.testsRun, len(self.failures), - len(self.errors))) - - def printResult(self): - self.printHeader() - self.printErrors() - self.printFailures() - -class JUnitTextTestRunner: - """A test runner class that displays results in textual form. - - The display format approximates that of JUnit's 'textui' test runner. - This test runner may be removed in a future version of PyUnit. - """ - def __init__(self, stream=sys.stderr): - self.stream = _WritelnDecorator(stream) - - def run(self, test): - "Run the given test case or test suite." - result = _JUnitTextTestResult(self.stream) - startTime = time.time() - test(result) - stopTime = time.time() - self.stream.writeln() - self.stream.writeln("Time: %.3fs" % float(stopTime - startTime)) - result.printResult() - return result - - -############################################################################## -# Verbose text UI -############################################################################## - -class _VerboseTextTestResult(TestResult): +class _TextTestResult(TestResult): """A test result class that can print formatted text results to a stream. - Used by VerboseTextTestRunner. + Used by TextTestRunner. """ - def __init__(self, stream, descriptions): + separator1 = '=' * 70 + separator2 = '-' * 70 + + def __init__(self, stream, descriptions, verbosity): TestResult.__init__(self) self.stream = stream - self.lastFailure = None + self.showAll = verbosity > 1 + self.dots = verbosity == 1 self.descriptions = descriptions - - def startTest(self, test): - TestResult.startTest(self, test) + + def getDescription(self, test): if self.descriptions: - self.stream.write(test.shortDescription() or str(test)) + return test.shortDescription() or str(test) else: - self.stream.write(str(test)) - self.stream.write(" ... ") + return str(test) - def stopTest(self, test): - TestResult.stopTest(self, test) - if self.lastFailure is not test: + def startTest(self, test): + TestResult.startTest(self, test) + if self.showAll: + self.stream.write(self.getDescription(test)) + self.stream.write(" ... ") + + def addSuccess(self, test): + TestResult.addSuccess(self, test) + if self.showAll: self.stream.writeln("ok") + elif self.dots: + self.stream.write('.') def addError(self, test, err): TestResult.addError(self, test, err) - self._printError("ERROR", test, err) - self.lastFailure = test + if self.showAll: + self.stream.writeln("ERROR") + elif self.dots: + self.stream.write('E') if err[0] is KeyboardInterrupt: self.shouldStop = 1 def addFailure(self, test, err): TestResult.addFailure(self, test, err) - self._printError("FAIL", test, err) - self.lastFailure = test - - def _printError(self, flavour, test, err): - errLines = [] - separator1 = "\t" + '=' * 70 - separator2 = "\t" + '-' * 70 - if not self.lastFailure is test: + if self.showAll: + self.stream.writeln("FAIL") + elif self.dots: + self.stream.write('F') + + def printErrors(self): + if self.dots or self.showAll: self.stream.writeln() - self.stream.writeln(separator1) - self.stream.writeln("\t%s" % flavour) - self.stream.writeln(separator2) - for line in apply(traceback.format_exception, err): - for l in string.split(line,"\n")[:-1]: - self.stream.writeln("\t%s" % l) - self.stream.writeln(separator1) + self.printErrorList('ERROR', self.errors) + self.printErrorList('FAIL', self.failures) + + def printErrorList(self, flavour, errors): + for test, err in errors: + self.stream.writeln(self.separator1) + self.stream.writeln("%s: %s" % (flavour,self.getDescription(test))) + self.stream.writeln(self.separator2) + for line in apply(traceback.format_exception, err): + for l in string.split(line,"\n")[:-1]: + self.stream.writeln("%s" % l) -class VerboseTextTestRunner: +class TextTestRunner: """A test runner class that displays results in textual form. It prints out the names of tests as they are run, errors as they occur, and a summary of the results at the end of the test run. """ - def __init__(self, stream=sys.stderr, descriptions=1): + def __init__(self, stream=sys.stderr, descriptions=1, verbosity=1): self.stream = _WritelnDecorator(stream) self.descriptions = descriptions + self.verbosity = verbosity + + def _makeResult(self): + return _TextTestResult(self.stream, self.descriptions, self.verbosity) def run(self, test): "Run the given test case or test suite." - result = _VerboseTextTestResult(self.stream, self.descriptions) + result = self._makeResult() startTime = time.time() test(result) stopTime = time.time() timeTaken = float(stopTime - startTime) - self.stream.writeln("-" * 78) + result.printErrors() + self.stream.writeln(result.separator2) run = result.testsRun self.stream.writeln("Ran %d test%s in %.3fs" % - (run, run > 1 and "s" or "", timeTaken)) + (run, run == 1 and "" or "s", timeTaken)) self.stream.writeln() if not result.wasSuccessful(): self.stream.write("FAILED (") @@ -605,9 +589,6 @@ class VerboseTextTestRunner: return result -# Which flavour of TextTestRunner is the default? -TextTestRunner = VerboseTextTestRunner - ############################################################################## # Facilities for running tests from the command line @@ -618,17 +599,22 @@ class TestProgram: for making test modules conveniently executable. """ USAGE = """\ -Usage: %(progName)s [-h|--help] [test[:(casename|prefix-)]] [...] +Usage: %(progName)s [options] [test[:(casename|prefix-)]] [...] + +Options: + -h, --help Show this message + -v, --verbose Verbose output + -q, --quiet Minimal output Examples: %(progName)s - run default set of tests %(progName)s MyTestSuite - run suite 'MyTestSuite' - %(progName)s MyTestCase:checkSomething - run MyTestCase.checkSomething - %(progName)s MyTestCase:check- - run all 'check*' test methods + %(progName)s MyTestCase.testSomething - run MyTestCase.testSomething + %(progName)s MyTestCase - run all 'test*' test methods in MyTestCase """ def __init__(self, module='__main__', defaultTest=None, - argv=None, testRunner=None): + argv=None, testRunner=None, testLoader=defaultTestLoader): if type(module) == type(''): self.module = __import__(module) for part in string.split(module,'.')[1:]: @@ -637,11 +623,12 @@ Examples: self.module = module if argv is None: argv = sys.argv + self.verbosity = 1 self.defaultTest = defaultTest self.testRunner = testRunner + self.testLoader = testLoader self.progName = os.path.basename(argv[0]) self.parseArgs(argv) - self.createTests() self.runTests() def usageExit(self, msg=None): @@ -652,29 +639,34 @@ Examples: def parseArgs(self, argv): import getopt try: - options, args = getopt.getopt(argv[1:], 'hH', ['help']) + options, args = getopt.getopt(argv[1:], 'hHvq', + ['help','verbose','quiet']) opts = {} for opt, value in options: if opt in ('-h','-H','--help'): self.usageExit() + if opt in ('-q','--quiet'): + self.verbosity = 0 + if opt in ('-v','--verbose'): + self.verbosity = 2 if len(args) == 0 and self.defaultTest is None: - raise getopt.error, "No default test is defined." + self.test = self.testLoader.loadTestsFromModule(self.module) + return if len(args) > 0: self.testNames = args else: self.testNames = (self.defaultTest,) + self.createTests() except getopt.error, msg: self.usageExit(msg) def createTests(self): - tests = [] - for testName in self.testNames: - tests.append(createTestInstance(testName, self.module)) - self.test = TestSuite(tests) + self.test = self.testLoader.loadTestsFromNames(self.testNames, + self.module) def runTests(self): if self.testRunner is None: - self.testRunner = TextTestRunner() + self.testRunner = TextTestRunner(verbosity=self.verbosity) result = self.testRunner.run(self.test) sys.exit(not result.wasSuccessful()) |