Defining test suites inside the test module

Each test module can provide one or more methods that define a different test suite. One method can exercise all the tests in a given module; another method can define a particular subset.

How to do it...

With the following steps, we will create some methods that define test suites using different means:

  1. Create a new file called recipe6.py in which to put our code for this recipe.
  2. Pick a class to test. In this case, we will use our Roman numeral converter.
    class RomanNumeralConverter(object):
      def __init__(self):
        self.digit_map = {"M":1000, "D":500, "C":100, "L":50, "X":10, "V":5, "I":1}
    
      def convert_to_decimal(self, roman_numeral):
        val = 0
        for char in roman_numeral:
          val += self.digit_map[char]
        return val
  3. Create a test class using the same name as the class under test with Test appended to the end.
    import unittest
    
    class RomanNumeralConverterTest(unittest.TestCase):
  4. Write a series of test methods, including a setUp method that creates a new instance of the RomanNumeralConverter for each test method.
    import unittest
    
    class RomanNumeralConverterTest(unittest.TestCase):
      def setUp(self):
        self.cvt = RomanNumeralConverter()
    
      def test_parsing_millenia(self):
        self.assertEquals(1000, 
                 self.cvt.convert_to_decimal("M"))
    
      def test_parsing_century(self):
        self.assertEquals(100, 
                 self.cvt.convert_to_decimal("C"))
    
      def test_parsing_half_century(self):
        self.assertEquals(50, 
                 self.cvt.convert_to_decimal("L"))
    
      def test_parsing_decade(self):
        self.assertEquals(10, 
                 self.cvt.convert_to_decimal("X"))
    
      def test_parsing_half_decade(self):
        self.assertEquals(5, self.cvt.convert_to_decimal("V"))
    
      def test_parsing_one(self):
        self.assertEquals(1, self.cvt.convert_to_decimal("I"))
    
      def test_empty_roman_numeral(self):
        self.assertTrue(self.cvt.convert_to_decimal("") == 0)
        self.assertFalse(self.cvt.convert_to_decimal("") > 0)
    
      def test_no_roman_numeral(self):
        self.assertRaises(TypeError, 
                 self.cvt.convert_to_decimal, None)
    
      def test_combo1(self):
        self.assertEquals(4000, 
                 self.cvt.convert_to_decimal("MMMM"))
    
      def test_combo2(self):
        self.assertEquals(2010, 
                 self.cvt.convert_to_decimal("MMX"))
    
      def test_combo3(self):
        self.assertEquals(4668, 
            self.cvt.convert_to_decimal("MMMMDCLXVIII"))
  5. Create some methods in the recipe's module (but not in the test case) that define different test suites.
    def high_and_low():
      suite = unittest.TestSuite()
      suite.addTest(
        RomanNumeralConverterTest("test_parsing_millenia"))
      suite.addTest(
        RomanNumeralConverterTest("test_parsing_one"))
      return suite
    
    def combos():
      return unittest.TestSuite(map(RomanNumeralConverterTest,
           ["test_combo1", "test_combo2", "test_combo3"]))
    
    def all():
      return unittest.TestLoader().loadTestsFromTestCase(
                    RomanNumeralConverterTest)
  6. Create a runner that will iterate over each of these test suites and run them through unittest's TextTestRunner.
    if __name__ == "__main__":
      for suite_func in [high_and_low, combos, all]:
        print "Running test suite '%s'" % suite_func.func_name
        suite = suite_func()
        unittest.TextTestRunner(verbosity=2).run(suite)
  7. Run the combination of test suites, and see the results.
    How to do it...

How it works...

We pick a class to test and define a number of test methods that check things out. Then we define a few module-level methods such as, high_and_low, combos, and all, to define test suites. Two of them contain fixed subsets of methods while all dynamically loads the test* methods from the class. Finally, the main part of our module iterates over a listing of all these functions that generate suites in order to smoothly create and run them.

There's more...

All of our test suites were run from the recipe's main runner. But this probably wouldn't be the case for a real project. Instead, the idea is to define different suites, and code a mechanism to pick which suite to run. Each suite is geared towards a different purpose, and it is necessary to allow the developer to pick which suite to run. This can be done by coding a command-line script using Python's optparse module to define command-line flags to pick one of these suites.

Test suite methods must be outside of the test class

If we make these suite-defining methods members of the test class, we would have to instantiate the test class. Classes that extend unittest.TestCase have a specialized init method that doesn't work well with an instance that is created just to call a non-test method. That is why the methods are outside the test class. While these methods can be in other modules, it is very convenient to define them inside the module containing the test code, to keep things in proximity.

Why have different suites?

What if we started our project off by running all tests? Sounds like a good idea, right? But what if the time to run the entire test suite grew to over an hour? There is a certain threshold after which developers tend to stop running tests, and nothing is worse than an un-run test suite. By defining subsets of tests, it is easy to run alternate suites during the day, and then perhaps run the comprehensive test suite once a day.

  • all is the comprehensive suite
  • high_and_low is an example of testing the edges
  • combos is a random sampling of values used to show that things are generally working

Defining our test suites is a judgment call. It's also worth it to re-evaluate each test suite every so often. If one test suite is getting too costly to run, consider moving some of its more expensive tests to another suite.

optparse is being phased out and replaced by argparse

While optparse is a convenient way to add command-line flags to Python scripts, it won't be available forever. Python 2.7 has deprecated this module and is continuing this development in argparse.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.147.193