Index: Doc/whatsnew/2.7.rst =================================================================== --- Doc/whatsnew/2.7.rst (revision 71941) +++ Doc/whatsnew/2.7.rst (working copy) @@ -473,6 +473,10 @@ to provide additional information about why the two objects are matching, much as the new sequence comparison methods do. + The :func:`unittest.main` now takes an optional ``exit`` argument. + If False ``main`` doesn't call :func:`sys.exit` allowing it to + be used from the interactive interpreter. :issue:`3379`. + * The :func:`is_zipfile` function in the :mod:`zipfile` module will now accept a file object, in addition to the path names accepted in earlier versions. (Contributed by Gabriel Genellina; :issue:`4756`.) Index: Doc/library/unittest.rst =================================================================== --- Doc/library/unittest.rst (revision 71941) +++ Doc/library/unittest.rst (working copy) @@ -1347,7 +1347,7 @@ applications which run test suites should provide alternate implementations. -.. function:: main([module[, defaultTest[, argv[, testRunner[, testLoader]]]]]) +.. function:: main([module[, defaultTest[, argv[, testRunner[, testLoader[, exit]]]]]]) 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 @@ -1357,4 +1357,15 @@ unittest.main() The *testRunner* argument can either be a test runner class or an already - created instance of it. + created instance of it. By default ``main`` calls :func:`sys.exit` with + an exit code indicated success or failure of the tests run. + + ``main`` supports being used from the interactive interpreter by passing in the + argument ``exit=False``. This displays the result on standard output without + calling :func:`sys.exit`:: + + >>> from unittest import main + >>> main(module='test_module', exit=False) + + Calling ``main`` actually returns an instance of the ``TestProgram`` class. + This stores the result of the tests run as the ``result`` attribute. Index: Lib/unittest.py =================================================================== --- Lib/unittest.py (revision 71941) +++ Lib/unittest.py (working copy) @@ -1469,7 +1469,7 @@ """ def __init__(self, module='__main__', defaultTest=None, argv=None, testRunner=TextTestRunner, - testLoader=defaultTestLoader): + testLoader=defaultTestLoader, exit=True): if isinstance(module, basestring): self.module = __import__(module) for part in module.split('.')[1:]: @@ -1478,6 +1478,8 @@ self.module = module if argv is None: argv = sys.argv + + self.exit = exit self.verbosity = 1 self.defaultTest = defaultTest self.testRunner = testRunner @@ -1529,15 +1531,12 @@ else: # it is assumed to be a TestRunner instance testRunner = self.testRunner - result = testRunner.run(self.test) - sys.exit(not result.wasSuccessful()) + self.result = testRunner.run(self.test) + if self.exit: + sys.exit(not self.result.wasSuccessful()) main = TestProgram -############################################################################## -# Executing this module from the command line -############################################################################## - if __name__ == "__main__": main(module=None) Index: Lib/test/test_unittest.py =================================================================== --- Lib/test/test_unittest.py (revision 71941) +++ Lib/test/test_unittest.py (working copy) @@ -9,9 +9,10 @@ import re from test import test_support import unittest -from unittest import TestCase +from unittest import TestCase, TestProgram import types from copy import deepcopy +from cStringIO import StringIO ### Support code ################################################################ @@ -3040,6 +3041,73 @@ "^unexpectedly identical: None : oops$"]) +class Test_TestProgram(TestCase): + + # Horrible white box test + def testNoExit(self): + result = object() + test = object() + + class FakeRunner(object): + def run(self, test): + self.test = test + return result + + 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: + TestProgram.parseArgs = oldParseArgs + del TestProgram.test + + + class FooBar(unittest.TestCase): + def testPass(self): + assert True + def testFail(self): + assert False + + class FooBarLoader(unittest.TestLoader): + """Test loader that returns a suite containing FooBar.""" + def loadTestsFromModule(self, module): + return self.suiteClass( + [self.loadTestsFromTestCase(Test_TestProgram.FooBar)]) + + + def test_NonExit(self): + program = unittest.main(exit=False, + testRunner=unittest.TextTestRunner(stream=StringIO()), + testLoader=self.FooBarLoader()) + self.assertTrue(hasattr(program, 'result')) + + + def test_Exit(self): + self.assertRaises( + SystemExit, + unittest.main, + testRunner=unittest.TextTestRunner(stream=StringIO()), + exit=True, + testLoader=self.FooBarLoader()) + + + def test_ExitAsDefault(self): + self.assertRaises( + SystemExit, + unittest.main, + testRunner=unittest.TextTestRunner(stream=StringIO()), + testLoader=self.FooBarLoader()) + + + ###################################################################### ## Main ###################################################################### @@ -3047,7 +3115,8 @@ def test_main(): test_support.run_unittest(Test_TestCase, Test_TestLoader, Test_TestSuite, Test_TestResult, Test_FunctionTestCase, - Test_TestSkipping, Test_Assertions, TestLongMessage) + Test_TestSkipping, Test_Assertions, TestLongMessage, + Test_TestProgram) if __name__ == "__main__": test_main()