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.
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.
recipe7.py
in which to put our application code that we will be testing.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
recipe7_legacy.py
to contain test code that doesn't use the unittest
module.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()
recipe7_pyunit.py
.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)
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
.
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.
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
.
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.
3.15.237.123