Retooling old test code to run inside unittest

Sometimes, we may have developed demo code to exercise our system. We don't have to rewrite it to run it inside unittest. Instead, it is easy to hook it up to the test framework and run it with some small changes.

How to do it...

With these steps, we will dive into capturing the test code that was written without using unittest, and repurposing it with minimal effort to run inside unittest.

  1. Create a file named recipe7.py in which to put our application code that we will be testing.
  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 new file named recipe7_legacy.py to contain test code that doesn't use the unittest module.
  4. Create a set of legacy tests that are coded, based on Python's assert function, not with unittest, along with a runner.
    from recipe7 import *
    
    class RomanNumeralTester(object):
      def __init__(self):
        self.cvt = RomanNumeralConverter()
    
      def simple_test(self):
        print "+++ Converting M to 1000"
        assert self.cvt.convert_to_decimal("M") == 1000
    
      def combo_test1(self):
        print "+++ Converting MMX to 2010"
        assert self.cvt.convert_to_decimal("MMXX") == 2010
    
      def combo_test2(self):
        print "+++ Converting MMMMDCLXVIII to 4668"
        val = self.cvt.convert_to_decimal("MMMMDCLXVII")
        self.check(val, 4668)
    
      def other_test(self):
        print "+++ Converting MMMM to 4000"
        val = self.cvt.convert_to_decimal("MMMM")
        self.check(val, 4000)
    
      def check(self, actual, expected):
        if (actual != expected):
          raise AssertionError("%s doesn't equal %s" % 
              (actual, expected))
    
      def test_the_system(self):
        self.simple_test()
        self.combo_test1()
        self.combo_test2()
        self.other_test()
    
    if __name__ == "__main__":
      tester = RomanNumeralTester()
      tester.test_the_system()

    Note

    This set of legacy tests is meant to represent legacy test code that our team has developed to exercise things before unittest was an option.

  5. Run the legacy tests. What is wrong with this situation? Did all the test methods run? Have we caught all the bugs?
    How to do it...
  6. Create a new file called recipe7_pyunit.py.
  7. Create a unittest set of tests, wrapping each legacy test method inside unittest's FunctionTestCase.
    from recipe7 import *
    from recipe7_legacy import *
    import unittest
    
    if __name__ == "__main__":
      tester = RomanNumeralTester()
    
      suite = unittest.TestSuite()
      for test in [tester.simple_test, tester.combo_test1, 
             tester.combo_test2, tester.other_test]:
        testcase = unittest.FunctionTestCase(test)
        suite.addTest(testcase)
    
      unittest.TextTestRunner(verbosity=2).run(suite)
  8. Run the unittest test. Did all the tests run this time? Which test failed? Where is the bug?
    How to do it...

How it works...

Python provides a convenient assert statement that tests a condition. When true, the code continues. When false, it raises an AssertionError. In the first test runner, we have several tests that check results using a mixture of assert statements or raising an AssertionError.

unittest provides a convenient class, unittest.FunctionTestCase, that wraps a bound function as a unittest test case. If an AssertionError is thrown, FunctionTestCase catches it, flags it as a test failure, and proceeds to the next test case. If any other type of exception is thrown, it will be flagged as a test error. In the second test runner, we wrap each of these legacy test methods with FunctionTestCase, and chain them together in a suite for unittest to run.

As seen by running the second test run, there is a bug lurking in the third test method. We were not aware of it, because the test suite was prematurely interrupted.

Another deficiency of Python's assert statement is shown by the first failure, as seen in the previous screenshot. When an assert fails, there is little to no information about the values that were compared. All we have is the line of code where it failed. The second assert in that screenshot was more useful, because we coded a custom checker that threw a custom AssertionError.

There's more...

Unittest does more than just run tests. It has a built-in mechanism to trap errors and failures, and then it continues running as much of our test suite as possible. This helps, because we can shake out more errors and fix more things within a given test run. This is especially important when a test suite grows to the point of taking minutes or hours to run.

Where are the bugs?

They exist in the test methods, and fundamentally were made by making slight alterations in the Roman numeral being converted.

  def combo_test1(self):
    print "+++ Converting MMX to 2010"
    assert self.cvt.convert_to_decimal("MMXX") == 2010

  def combo_test2(self):
    print "+++ Converting MMMMDCLXVIII to 4668"
    val = self.cvt.convert_to_decimal("MMMMDCLXVII")
    self.check(val, 4668)

The combo_test1 test method prints out that it is converting MMX, but actually tries to convert MMXX. The combo_test2 test method prints out that it is converting MMMMDCLXVIII, but actually tries to convert MMMMDCLXVII.

This is a contrived example, but have you ever run into bugs just as small that drove you mad trying to track them down? The point is, showing how easy or hard it can be to track them down is based on how the values are checked. Python's assert statement isn't very effective at telling us what values are compared where. The customized check method is much better at pinpointing the problem with combo_test2.

Tip

This highlights the problem with having comments or print statements trying to reflect what the asserts do. They can easily get out of sync and the developer may face some problems trying to track down bugs. Avoiding this situation is known as the DRY principle (Don't Repeat Yourself).

FunctionTestCase is a temporary measure

The FunctionTestCase is a test case that provides an easy way to quickly migrate tests based on Python's assert statement, so they can be run with unittest. But things shouldn't stop there. If we take the time to convert RomanNumeralTester into a unittest TestCase, then we gain access to other useful features like the various assert* methods that come with TestCase. It's a good investment. The FunctionTestCase just lowers the bar to migrate to unittest.

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

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