Test classes are created by subclassing TestCase . The simplest approach is to override the method runTest( ) , as shown in Example 9-1. Example 9-1. Simple unit test for the class Book booktests.py """Unit test for book.py""" import book import unittest class BookTests (unittest.TestCase): def runTest (self): """Test book creation""" book1 = book.Book( "Cosmos", "Carl Sagan" ) self. assertEqual ( "Cosmos", book1.title ) This example creates the test class BookTests . The test method runTest( ) creates a Book and uses the assertEqual( ) test assert method to verify its attributes. Test methods customarily contain a label similar to the example's Test book creation . This test description is printed if the test fails. The class Book tested by BookTests is given in Example 9-2. Example 9-2. Simple unit test for the class Book book.py class Book : title = "" author = "" def __init_ _ (self, title, author): self.title = title self.author = author Unit tests may be run from the command line using unittests.py as a test runner. The argument specifies the Python module name, class name, and method name of the test to run. Example 9-3 demonstrates running BookTests this way and shows the result. Example 9-3. Results of running BookTests $ python unittest.py booktests.BookTests.runTest . ------------------------------------------------ Ran 1 test in 0.000s OK For this command to work as shown, the module unittests.py must be present in the Python search path specified by the environment variable $PYTHONPATH . Making BookTests fail by changing the test assert statement to self.assertEqual ( " Bad", book1.title ) demonstrates PyUnit's test failure reporting, as shown in Example 9-4. Example 9-4. BookTests failure $ python unittest.py booktests.BookTests.runTest F ====================================================================== FAIL: Test book creation ---------------------------------------------------------------------- Traceback (most recent call last): File "/cygdrive/c/work/UnitTestFrameworks/Python/example1/booktests. py", line 11, in runTest self.assertEqual( "Bad", book1.title ) File "/cygdrive/c/work/UnitTestFrameworks/Python/example1/unittest. py", line 302, in failUnlessEqual raise self.failureException, \ AssertionError: 'Bad' != 'Cosmos' ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (failures=1) The failure report includes the test description Test book creation , as well as the specific assert condition that failed. Rather than overriding runTest( ) , it is far more common to create uniquely named test methods. This allows building test classes with multiple test methods. Example 9-5 shows BookTests redesigned this way. Example 9-5. Redesigned BookTest booktests.py """Unit test for book.py""" import book import unittest class BookTests (unittest.TestCase): def testCreateBook (self): """Test book creation""" book1 = book.Book( "Cosmos", "Carl Sagan" ) self.assertEqual( "Cosmos", book1.title ) if __name__ == '__main_ _': unittest.main( ) The additional two lines of code at the end allow the test to be run directly without using unittest.py , as shown in Example 9-6. All methods that have names starting with test are found and run. Example 9-6. Running BookTest directly $ python booktests.py . --------------------- Ran 1 test in 0.000s OK With this approach, multiple test methods can be added to a test class. If they share objects, test fixture behavior should be implemented using the setUp() and tearDown( ) methods. Example 9-7 shows the test fixture LibraryTests . Example 9-7. . The test class LibraryTests librarytests.py """Unit test for library.py""" import book import library import unittest class LibraryTests (unittest.TestCase): def setUp (self): self.library = library.Library( ) book1 = book.Book( "Cosmos", "Carl Sagan" ) self.library.addBook( book1 ) book2 = book.Book( "Contact", "Carl Sagan" ) self.library.addBook( book2 ) def tearDown (self): self.library.dispose( ) def testGetNumBooks (self): """Test getting number of books""" self. assert_ ( self.library.getNumBooks( )==2 ) def testGetBook (self): """Test getting a book from library""" book2 = self.library.getBook( "Cosmos" ) self.assertNotEqual( None, book2, "Book not found" ) The setUp( ) method creates a Library and adds two Book s to it, and tearDown( ) disposes of the Library . The test method testGetNumBooks( ) uses the test assert method assert_() to check the library's size . This is the most generic type of test assert, as it simply checks whether its argument evaluates to true . Example 9-8 shows the Library class that is tested by LibraryTest . Example 9-8. The Library class library.py class NonexistentBookError (Exception): """Exception thrown for missing book""" pass class Library : """A library""" def __init_ _ (self): self._ _books = dict( ) def dispose (self): self._ _books.clear( ) def addBook (self, book): """Add a book""" self._ _books[book.title] = book def getBook (self, title): """Find a book by title""" return self._ _books.get(title) def removeBook (self, title): """Remove a book""" if self._ _books.has_key(title): self._ _books.pop(title) else: raise NonexistentBookError def getNumBooks (self): """Get number of books""" return len(self._ _books) Library uses the Python dictionary object dict( ) to contain a collection of Book s. The module library.py also defines the exception class NonexistentBookError . This type of exception is thrown by the method removeBook( ) if it cannot find the Book to remove. The failUnlessRaises() test assert method can be used to check for expected exception behavior, as shown in Example 9-9. Example 9-9. Testing for an expected exception librarytests.py def testRemoveNonexistentBook (self): """Test expected exception from removing a nonexistent book""" self. failUnlessRaises (library.NonexistentBookError, self.library.removeBook, "Nonexistent" ) The arguments passed to failUnlessRaises( ) are an exception type, a callable object, and a variable argument list. In this example, the exception type is NonexistentBookError and the callable object is the function removeBook( ) . The object is called with the specified argument list. If an exception of the given exception type is thrown, the test passes . If no exception is thrown, or some other type of error occurs, the test fails. Multiple tests may be aggregated using TestSuite . Example 9-10 adds a function named suite() to create a TestSuite containing LibraryTest 's test methods and changes the call to run the suite to unittest.main() . Example 9-10. Creating and running a TestSuite librarytests.py def suite ( ): suite = unittest. TestSuite ( ) suite. addTest (LibraryTests("testGetNumBooks")) suite. addTest (LibraryTests("testGetBook")) suite. addTest (LibraryTests("testRemoveNonexistentBook")) return suite if __name__ == '__main_ _': unittest.main(defaultTest='suite') Tests are added to a TestSuite using its addTest( ) method. It also has an addTests( ) method that allows multiple tests to be added at once. PyUnit provides a convenience method, makeSuite() , which creates a TestSuite . It finds all methods named with a given prefix, such as test , and returns a suite containing them. Example 9-11 demonstrates makeSuite( ) . Example 9-11. . Using makeSuite( ) to create a TestSuite librarytests.py def suite ( ): suite = unittest. makeSuite (LibraryTests, "test") return suite It's often useful to create a module that builds TestSuite containing all the tests in each test class. Example 9-12 shows such a module, named alltests.py . Example 9-12. Module to run all tests alltests.py import unittest def suite ( ): modules_to_test = (' booktests ', ' librarytests ') alltests = unittest. TestSuite ( ) for module in map(__import_ _, modules_to_test): alltests.addTest(unittest.findTestCases(module)) return alltests if __name__ == '__main_ _': unittest.main(defaultTest='suite') This example creates and runs a TestSuite named alltests that contains all the tests from booktests.py and librarytests.py . Python includes a command-line interpreter for interactively running code. PyUnit tests can be run this way. Example 9-13 demonstrates using the interpreter to create and run a unit test. Example 9-13. Running a test interactively $ python >>> import unittest >>> import librarytests >>> runner = unittest.TextTestRunner( ) >>> test = librarytests.LibraryTests("testGetBook") >>> runner.run(test) . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK <unittest._TextTestResult run=1 errors=0 failures=0> In this example, the modules unittest and librarytests are imported and a TextTestRunner is created. Next, a test containing the test method testGetBook( ) is created and run using the test runner. A TestSuite can be created and run similarly, as shown in Example 9-14. Example 9-14. Creating a TestSuite interactively >>> suite = unittest.makeSuite(librarytests.LibraryTests,'test') >>> runner.run(suite) ..... ---------------------------------------------------------------------- Ran 5 tests in 0.001s OK <unittest._TextTestResult run=5 errors=0 failures=0> Example 9-15 illustrates creating a test from BookTests and adding it to the TestSuite . Example 9-15. Adding a test to the test suite >>> import booktests >>> test2 = booktests.BookTests("testCreateBook") >>> suite.addTest(test2) >>> runner.run(suite) ...... ---------------------------------------------------- Ran 6 tests in 0.001s OK <unittest._TextTestResult run=6 errors=0 failures=0> The PyUnit GUI is implemented in the module unittestgui.py . It is not included with the standard Python libraries but is available in downloads from the PyUnit web site. It acts as a test runner, running test modules, classes, and methods, and displays a friendly green or red test results indicator, as well as failure details. It is shown in Figure 9-1. Figure 9-1. The PyUnit GUI The name of the test to run is entered at the top. The name can specify a module (e.g., librarytests ), a test class ( librarytests.LibraryTests ), or a test method ( librarytests.LibraryTests.testGetBook ). Except when a specific test method name is specified, all the methods in the module or class being tested that have names starting with test are run. |