Python Unit Testing Framework

来源:百度文库 编辑:神马文学网 时间:2024/04/29 05:26:43
Author: Steve Purcell,
Project website:http://pyunit.sourceforge.net/
Contents
OverviewSystem RequirementsUsing PyUnit to write your own testsInstallationAn introduction to TestCasesCreating a simple test caseRe-using set-up code: creating ‘fixtures‘TestCase classes with several test methodsAggregating test cases into test suitesNesting test suitesWhere to place testing codeRunning tests interactivelyRunning tests from the command lineThe GUI test runnerDocumenting your testsMore about test conditionsTesting for equalityTesting for exceptions
Re-using old test code with PyUnitUsing PyUnit with JPython and JythonCaveatsAssertionsMemory use
Terms of useFuture plansUpdates and communityAcknowledgementsRelated informationAbout the author
The Python unit testing framework, dubbed ‘PyUnit‘ by convention, is aPython language version ofJUnit, by smart cookies Kent Beck and Erich Gamma. JUnit is, in turn, a Java version of Kent‘s Smalltalk testing framework. Each is the de facto standard unit testing framework for its respective language.
This document explains the Python-specific aspects of the design and usage of PyUnit; for background information on the basic design of the framework the reader is referred to Kent‘s original paper,"Simple Smalltalk Testing: With Patterns".
PyUnit forms a part of the Python Standard Library as of Pythonversion 2.1.
The following information assumes knowledge ofPython, a language so easy that even I managed to learn it, and so addictive that I can‘t stop.
PyUnit is designed to work with any standard Python, version 1.5.2 and higher.
PyUnit has been tested by the author on Linux (Redhat 6.0 and 6.1, Debian Potato) with Python 1.5.2, 2.0 and 2.1. It is also known to work on other Python platforms, including Windows and Mac. If any platform or Python version issues cause you trouble, please let me know.
For details of using PyUnit with JPython and Jython, please refer to the section‘Using PyUnit with JPython and Jython‘.
The classes needed to write tests are to be found in the ‘unittest‘ module. This module is part of the standard Python library for Python 2.1 and later. If you are using an older Python version, you should obtain the module from the separate PyUnit distribution.
To be able to use the module from your own code, simply ensure that the directory containing the file ‘unittest.py‘ is in your Python search path. You can do this by setting the ‘$PYTHONPATH‘ environment variable, or by placing the file in a directory in your current Python search path, such as /usr/lib/python1.5/site-packages on Redhat Linux machines.
Note that you will have to do this before you can run the examples that are provided with PyUnit unless you copy ‘unittest.py‘ into the examples directory.
The basic building blocks of unit testing are ‘test cases‘ -- single scenarios that must be set up and checked for correctness. In PyUnit, test cases are represented by the TestCase class in the unittest module. To make your own test cases you must write subclasses of TestCase.
An instance of a TestCase class is an object that can completely run a single test method, together with optional set-up and tidy-up code.
The testing code of a TestCase instance should be entirely self contained, such that it can be run either in isolation or in arbitrary combination with any number of other test cases.
The simplest test case subclass will simply override the runTest method in order to perform specific testing code:
import unittestclass DefaultWidgetSizeTestCase(unittest.TestCase):def runTest(self):widget = Widget("The widget")assert widget.size() == (50,50), ‘incorrect default size‘
Note that in order to test something, we just use the built-in ‘assert‘ statement of Python. If the assertion fails when the test case runs, an AssertionError will be raised, and the testing framework will identify the test case as a ‘failure‘. Other exceptions that do not arise from explicit ‘assert‘ checks are identified by the testing framework as ‘errors‘. (See also the section‘More about test conditions‘.)
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 arguments:
testCase = DefaultWidgetSizeTestCase()
Now, such test cases can be numerous, and their set-up can be repetitive. In the above case, constructing a ‘Widget‘ in each of 100 Widget test case subclasses would mean unsightly duplication.
Luckily, we can factor out such set-up code by implementing a hook method called setUp, which the testing framework will automatically call for us when we run the test:
import unittestclass SimpleWidgetTestCase(unittest.TestCase):def setUp(self):self.widget = Widget("The widget")class DefaultWidgetSizeTestCase(SimpleWidgetTestCase):def runTest(self):assert self.widget.size() == (50,50), ‘incorrect default size‘class WidgetResizeTestCase(SimpleWidgetTestCase):def runTest(self):self.widget.resize(100,150)assert self.widget.size() == (100,150), ‘wrong size after resize‘
If the setUp method raises an exception while the test is running, the framework will consider the test to have suffered an error, and the runTest method will not be executed.
Similarly, we can provide a tearDown method that tidies up after the runTest method has been run:
import unittestclass SimpleWidgetTestCase(unittest.TestCase):def setUp(self):self.widget = Widget("The widget")def tearDown(self):self.widget.dispose()self.widget = None
If setUp succeeded, the tearDown method will be run regardless of whether or not runTest succeeded.
Such a working environment for the testing code is termed a fixture.
Often, many small test cases will use the same fixture. In this case, we would end up subclassing SimpleWidgetTestCase into many small one-method classes such as DefaultWidgetSizeTestCase. This is time-consuming and discouraging, so in the same vein as JUnit, PyUnit provides a simpler mechanism:
import unittestclass WidgetTestCase(unittest.TestCase):def setUp(self):self.widget = Widget("The widget")def tearDown(self):self.widget.dispose()self.widget = Nonedef testDefaultSize(self):assert self.widget.size() == (50,50), ‘incorrect default size‘def testResize(self):self.widget.resize(100,150)assert self.widget.size() == (100,150), ‘wrong size after resize‘
Here we have not provided a runTest method, but have instead provided two different test methods. Class instances will now each run one of the test methods, with self.widget created and destroyed separately for each instance. When creating an instance we must specify the test method it is to run. We do this by passing the method name in the constructor:
defaultSizeTestCase = WidgetTestCase("testDefaultSize")resizeTestCase = WidgetTestCase("testResize")
Test case instances are grouped together according to the features they test. PyUnit provides a mechanism for this: the ‘test suite‘, represented by the class TestSuite in the unittest module:
widgetTestSuite = unittest.TestSuite()widgetTestSuite.addTest(WidgetTestCase("testDefaultSize"))widgetTestSuite.addTest(WidgetTestCase("testResize"))
For the ease of running tests, as we will see later, it is a good idea to provide in each test module a ‘callable‘ object that returns a pre-built test suite:
def suite():suite = unittest.TestSuite()suite.addTest(WidgetTestCase("testDefaultSize"))suite.addTest(WidgetTestCase("testResize"))return suite
or even:
class WidgetTestSuite(unittest.TestSuite):def __init__(self):unittest.TestSuite.__init__(self,map(WidgetTestCase,("testDefaultSize","testResize")))
(the latter is admittedly not for the faint-hearted)
Since it is a common pattern to create a TestCase subclass with many similarly named test functions, there is a convenience function called makeSuite provided in the unittest module that constructs a test suite that comprises all of the test cases in a test case class:-
suite = unittest.makeSuite(WidgetTestCase,‘test‘)
Note that when using the makeSuite function, the order in which the various test cases will be run by the test suite is the order determined by sorting the test function names using the cmp built-in function.
Often it is desirable to group suites of test cases together, so as to run tests for the whole system at once. This is easy, since TestSuites can be added to a TestSuite just as TestCases can be added to a TestSuite:-
suite1 = module1.TheTestSuite()suite2 = module2.TheTestSuite()alltests = unittest.TestSuite((suite1, suite2))
An example of nesting test suites can be found in the file ‘alltests.py‘, in the ‘examples‘ subdirectory of the distribution package.
You can place the definitions of test cases and test suites in the same modules as the code they are to test (e.g. ‘widget.py‘), but there are several advantages to placing the test code in a separate module, such as ‘widgettests.py‘:
The test module can be run standalone from the command line The test code can more easily be separated from shipped code There is less temptation to change test code to fit the code it tests without a good reason Test code should be modified much less frequently than the code it tests Tested code can be refactored more easily Tests for modules written in C must be in separate modules anyway, so why not be consistent? If the testing strategy changes, there is no need to change the source code
Of course, the whole point of writing these tests is so that we can run them and find out if our software is working. The test framework uses ‘TestRunner‘ classes to provide an environment in which your tests can execute. The most common TestRunner is TextTestRunner, which can run tests and report the results in textual form:
runner = unittest.TextTestRunner()runner.run(widgetTestSuite)
By default, TextTestRunner prints its output to sys.stderr, but this can be changed by passing a different file-like object to its constructor.
Using TextTestRunner like this is an ideal way to run your tests interactively from within a Python interpreter session.
The unittest module contains a function called main, which can be used to easily turn a test module into a script that will run the tests it contains. The main function uses the unittest.TestLoader class to automatically find and load test cases within the current module.
Therefore, if you name your test methods using the test* convention described earlier, you can place the following code at the bottom of your test module:
if __name__ == "__main__":unittest.main()
Then, when you execute your test module from the command line, all of the tests contained therein will be run. Run the module with the ‘-h‘ option in order to see the options available.
To run arbitrary tests from the command-line, you can run the unittest module as a script, giving it the name of an test case or test suite:
% python unittest.py widgettests.WidgetTestSuite
or
% python unittest.py widgettests.makeWidgetTestSuite
You may also specify particular tests on the command-line. To run the TestCase subclass ‘ListTestCase‘ in the module ‘listtests‘ (see the ‘examples‘ subdirectory of the distribution package) you can execute the command:
% python unittest.py listtests.ListTestCase.testAppend
where ‘testAppend‘ is the name of the test method that is to be run by the test case instance. To create and run ListTestCase instances for all the ‘test*‘ methods in that class, you can run:
% python unittest.py listtests.ListTestCase
There is a graphical front end that you can use in order to run your tests. It is written using Tkinter, the windowing toolkit shipped with Python on most platforms. It looks similar to the JUnit GUI.
To use the GUI test runner, simply run:
% python unittestgui.pyor % python unittestgui.py widgettests.WidgetTestSuite
Note that here, again, the name entered for the test to be run should be the fully-qualified name of an object which returns a TestCase or TestSuite instance. It should not be the name of a pre-created test, since every test must be recreated each time it is run.
The use of the GUI test runner rather than the text test runner imposes a time overhead due to all those window updates; on my system, it takes an extra seven seconds per thousand tests. Your mileage may vary.
Usually, when a test is run its name is displayed by the TestRunner. This name is derived from the name of the test case class, and the name of the test method that the instance has been initialised to run.
However, if you supply a doc-string for a test method, the first line of that doc-string will be displayed when the test is run. This provides an easy mechanism for documenting your tests:
class WidgetTestCase(unittest.TestCase):def testDefaultSize(self):"""Check that widgets are created with correct default size"""assert self.widget.size() == (50,50), ‘incorrect default size‘
I have suggested the use of Python‘s built-in assertion mechanism for checking conditions in test cases rather than a ‘home-brewed‘ equivalent; assert is simple, concise and familiar.
Note, however, that if tests are run with Python‘s optimisation option turned on (generating ‘.pyo‘ bytecode files), the assert statements will be skipped, rendering the test cases quite useless.
For those who tend to work with Python‘s optimisation option enabled, I have included a method assert_ in the TestCase class. It is functionally equivalent to the assert built-in and will not be optimised away, but it is less convenient and results in less helpful error messages:
def runTest(self):self.assert_(self.widget.size() == (100,100), "size is wrong")
For good measure I have also provided TestCase with failIf and failUnless methods:
def runTest(self):self.failIf(self.widget.size() <> (100,100))
A test case method can also call fail in order to fail immediately:
def runTest(self):...if not hasattr(something, "blah"):self.fail("blah missing")# or just ‘self.fail()‘
The most common type of assertion is an assertion of equality between two values or objects. If the assertion fails, the developer usually wants to see what the incorrect value actually was.
TestCase has a pair of methods called assertEqual and assertNotEqual for this purpose (with aliases failUnlessEqual and failIfEqual for those who prefer):
def testSomething(self):self.widget.resize(100,100)self.assertEqual(self.widget.size, (100,100))
Often a test will wish to check that an exception is raised in a certain set of circumstances. If the expected exception is not thrown, the test should fail. This is easy to do:
def runTest(self):try:self.widget.resize(-1,-1)except ValueError:passelse:fail("expected a ValueError")
Usually, the source of the expected exception is a callable object; for that reason, TestCase has an assertRaises method. The first two arguments of the method are the expected exception as it would appear in an ‘except‘ clause, and the callable object. The remaining arguments are those that should be passed to the callable object:
def runTest(self):self.assertRaises(ValueError, self.widget.resize, -1, -1)
Some users will find that they have existing test code that they would like to run from PyUnit, without converting every old test function to a TestCase subclass.
For this reason, PyUnit provides a FunctionTestCase class. This subclass of TestCase can be used to wrap an existing test function. Set-up and tear-down functions can also optionally be wrapped.
Given the following test function:
def testSomething():something = makeSomething()assert something.name is not None...
one can create an equivalent test case instance as follows:
testcase = unittest.FunctionTestCase(testSomething)
If there are additional set-up and tear-down methods that should be called as part of the test case‘s operation, they can also be provided:
testcase = unittest.FunctionTestCase(testSomething,setUp=makeSomethingDB,tearDown=deleteSomethingDB)
Although PyUnit was written primarily for ‘C‘ Python, it is possible to write PyUnit tests using Jython for your Java or Jython software. This can be preferable to trying to write JUnit tests using Jython. PyUnit also works correctly with Jython‘s predecessors, JPython 1.0 and 1.1.
Of course, Java does not have a TK GUI interface, so PyUnit‘s Tkinter-based GUI will not work with Jython. The text-only interface works just fine, however.
To do so, simply copy the standard C Python library module files ‘traceback.py‘, ‘linecache.py‘, ‘stat.py‘ and ‘getopt.py‘ to a location from which they can be imported by JPython. You can get these files from any distribution of C Python. (These guidelines are based on the standard library of C Python 1.5.x, and may not be correct for other Python versions.)
Now you can write your PyUnit tests exactly as you would with C Python.
See the caveats in the section"More about test conditions" above.
When exceptions are raised during the running of a test suite, the resulting traceback objects are saved so that failure details can be formatted and printed at the end of the test run. Apart from simplicity, the benefit of this is that a future version of the GUI TestRunner will be able to allow post-mortem inspection of local and global variable values, which are stored with the traceback.
A possible side-effect is that when running test suites with very high failure rates, the memory usage of all these saved traceback objects could become a problem. Of course, if so many tests are failing, this memory overhead is the least of your problems.
You may freely use, alter and redistribute this software under the same liberal terms that apply to Python itself. All I ask is that my name, e-mail address and the project URL be retained in the source code and accompanying documentation, with a credit for me as the original author.
My motive for writing this software was to make a small contribution to the improvement of software quality in the world; I didn‘t bargain on getting any cash. (That‘s not to say that sponsorship would be unwelcome.)
One key plan for the future is to integrate the TK GUI with the IDLE IDE. Volunteers are welcome!
Other than that, I have no great plans to extend the functionality of the module. I have kept PyUnit as simple as possible (but no simpler, hopefully!) because I believe that helper modules for such common testing tasks as log file comparison are better written by test writers than by myself.
News, updates and more are available at theproject website.
Comments, suggestions and bug reports are welcome; simply e-mail me or, better still, join the very low-volumemailing list and post your comments there. There are a surprisingly large number of people already using PyUnit, and they all have wisdom to share.
Many thanks to Guido and his disciples for the Python language. In tribute, I have written the following haiku (or ‘pyku‘, if you will):
Guido van Rossum
‘Gawky Dutchman‘ gave birth to
Beautiful Python
I gratefully acknowledge the work of Kent Beck and Erich Gamma for their work on JUnit, which made the design of PyUnit a no-brainer.
Thanks also to Tim Voght; I discovered after I had implemented PyUnit that he had also implemented a ‘pyunit‘ module as part of his ‘PyWiki‘ WikiWikiWeb clone. He graciously gave me the go-ahead to submit my version to the community at large.
Many thanks to those who have written to me with suggestions and questions. I‘ve tried to add appropriate credits in the CHANGES file in the download package.
Particular thanks to Jérôme Marant, who packaged PyUnit for Debian.
PyUnit websitePython website"Simple Smalltalk Testing: With Patterns" by Kent BeckJUnit websiteXProgramming.com -- the home of eXtreme ProgrammingExtremeProgramming on WikiWikiWebPyWiki -- Python WikiWikiWeb clone by Tim Voght
Steve Purcell isjust a programmer at heart, working independently writing, applying and teaching Open Source software.
He recently acted as Technical Director for a Web/WAP start-up, but spends most of his time architecting and coding large Java systems whilst counterproductively urging his Java-skilled colleagues to take up Python instead.
Steve Purcell, $Id: PyUnit.html,v 1.20 2001/08/08 07:22:16 purcell Exp $