Python - Unit Testing
TDD using unittest.TestCase class 2020
In this chapter, we're going to write and debug a set of functions to convert to and from Hexa numbers.
We can start mapping out what a hexa.py module should do. It will have two main functions, toHexa() and fromHexa(). The toHexa() function should take an integer from 1 to 65536 and return the hexa numeral representation as a string.
We want test our function toHexa() even before we start to writing. This is so called test-driven development (TDD).
Python has a framework for unit testing named unittest module.
The toHexa() function must return the hexa number for all integers from 1 to 65536.
# hexatest.py import hexa # This is the code being tested import unittest class Samples(unittest.TestCase): pair = ( (1,'1'), (2,'2'), (3,'3'), (4,'4'), (5,'5'), (6,'6'), (7,'7'), (8,'8'), (9,'9'), (10,'A'), (11,'B'), (12,'C'), (13,'D'), (14,'E'), (15,'F'), (16,'10'), (30,'1E'), (127,'7F'), (255,'FF'), (500,'1F4'), (1024,'400'), (5100,'13EC'), (65535,'FFFF'), (65536,'10000') ) def testToHexa(self): for d, h in self.pair: res = hexa.toHexa(d); self.assertEqual(h, res) if __name__ == '__main__': unittest.main()
- We subclassed the TestCase class of the unittest module:
class Samples(unittest.TestCase):
The TestCase class provides many useful methods which we can use in our test case to test specific conditions. - The pair is a tuple of decimal/hexa pairs to test every possible input, but we should try to test all the obvious edge cases.
- Here we call the toHexa() function which hasn't been written yet, but once it is, this is the line that will call it. The toHexa() function takes an integer and return a string for hexa.
- The TestCase class provides a method, assertEqual(), to check whether the return value from toHexa() and haxa value of pair match. If they don't match, assertEqual() will raise an exception and the test will fail. If the two values are equal, assertEqual() will do nothing.
Once we have a test case, we can start coding the toHexa() function.
First, we should stub it out as an empty function and make sure the tests fail. If the tests succeed before we've written any code, our tests aren't testing our code at all. We should write a test that fails, then code until it passes.
# hexa.py def toHexa(n) '''convert decimal to hexa''' pass
If we test run with hexatest.py, we get the following output:
$ python hexatest.py -v testToHexa (__main__.Samples) ... FAIL ====================================================================== FAIL: testToHexa (__main__.Samples) ---------------------------------------------------------------------- Traceback (most recent call last): File "hexatest.py", line 15, in testToHexa self.assertEqual(h, res) AssertionError: '1' != None ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1)
When we run the hexatest.py script, it acually runs unittest.main(). Then, it runs each test case which is a method within a class in hexatest.py. Note that each test class must inherit from unittest.TestCase.
For each failed test case, unittest displays the trace information showing exactly what happened. In this case, the call to assertEqual() raised an AssertionError because it was expecting toHex(1) to return '1', but it didn't. That's because there was no explicit return statement, the function returned None, the Python null value.
When a test case doesn't pass, unittest distinguishes between failures and errors:
- A failure is a call to assertEqual or assertRaises, that fails because the asserted condition is not true or the expected exception was not raised.
- An error is any other sort of exception raised in the code you're testing or the unit test case itself.
# hexa.py def toHexa(n): '''convert decimal to hexa''' map = ('0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F') s = '' while(n/16): s += map[n % 16] n = n/16 s += map[n % 16] # reverse return s[::-1]
Let's test the code interactively:
>>> from hexa import toHexa >>> toHexa(10) 'A' >>> toHexa(17) '11' >>> toHexa(255) 'FF' >>> toHexa(5100) '13EC' >>> toHexa(65535) 'FFFF' >>> toHexa(65536) '10000'
At least the code passed manual test.
But will it pass the test case you wrote?
python hexatest.py -v testToHexa (__main__.Samples) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.001s OK
It passed our the test case!
Passing the test with good input is not enough. We should also test whether they fail when given bad input. Not just any sort of failure but they must fail in the way we expect.
The toHex() function should raise an OutOfRangeError when given an integer is negative integer. Of course we can fully implement for this negative case, but for the purpose of testing negative integer case, we didn't do it.
Here is the test case file, hexatest2.py:
# hexatest2.py import hexa2 # This is the code being tested import unittest class Samples(unittest.TestCase): pair = ( (1,'1'), (2,'2'), (3,'3'), (4,'4'), (5,'5'), (6,'6'), (7,'7'), (8,'8'), (9,'9'), (10,'A'), (11,'B'), (12,'C'), (13,'D'), (14,'E'), (15,'F'), (16,'10'), (30,'1E'), (127,'7F'), (255,'FF'), (500,'1F4'), (1024,'400'), (5100,'13EC'), (65535,'FFFF'), (65536,'10000') ) def testToHexa(self): for d, h in self.pair: res = hexa2.toHexa(d); self.assertEqual(h, res) class BadSamples(unittest.TestCase): def testToHexaNegative(self): ''' toHex should fail when the input < 0''' self.assertRaises(hexa2.OutOfRangeError, hexa2.toHexa, -1) if __name__ == '__main__': unittest.main()
The unittest.TestCase class provides the assertRaises method, which takes the following arguments: the exception we're expecting, the function we're testing, and the arguments we're passing to that function.
The hexa2.py has just pass:
# hexa2.py def toHexa(n): '''convert decimal to hexa''' pass
If we run the test:
$ python hexatest2.py -v testToHexaNegative (__main__.BadSamples) toHex should fail when the input < 0 ... ERROR testToHexa (__main__.Samples) ... FAIL ====================================================================== ERROR: testToHexaNegative (__main__.BadSamples) toHex should fail when the input < 0 ---------------------------------------------------------------------- Traceback (most recent call last): File "hexatest2.py", line 21, in testToHexaNegative self.assertRaises(hexa2.OutOfRangeError, hexa2.toHex, -1) AttributeError: 'module' object has no attribute 'OutOfRangeError' ====================================================================== FAIL: testToHexa (__main__.Samples) ---------------------------------------------------------------------- Traceback (most recent call last): File "hexatest2.py", line 16, in testToHexa self.assertEqual(h, res) AssertionError: '1' != None ---------------------------------------------------------------------- Ran 2 tests in 0.017s FAILED (failures=1, errors=1)
Looks like we failed. Well, the traceback tells that the module we're testing doesn't have an exception called OutOfRangeError. We passed this exception to the assertRaises() method, because it's the exception we want the function to raise given an out-of-range input. But the exception doesn't exist, so the call to the assertRaises() method failed. It never got a chance to test the toHex() function; it didn't get that far.
So, we need to modify our hexa2.py file to define the OutOfRangeError exception. Now we can write the code to make this test pass.
# hexa2.py class OutOfRangeError(ValueError): pass def toHexa(n): '''convert decimal to hexa''' if(n < 0): raise OutOfRangeError('number out of range, n < 0 )') map = ('0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F') s = '' while(n/16): s += map[n % 16] n = n/16 s += map[n % 16] # reverse return s[::-1]
if the given input (n) is less than 0, raise an OutOfRangeError exception.
python hexatest2.py -v testToHexaNegative (__main__.BadSamples) toHex should fail when the input < 0 ... ok testToHexa (__main__.Samples) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.005s OK
The dec to hexa conversion code seems to work for big numbers. So, we're not going to check for the big numbers.
How about the non-integer numbers:
$ python >>> from hexa2 import toHexa >>> toHexa(5.0) Traceback (most recent call last): File "", line 1, in File "hexa2.py", line 16, in toHexa s += map[n % 16] TypeError: tuple indices must be integers, not float
Testing for non-integers is not tough. We need to define a NotIntegerError exception.
# hexa3.py class OutOfRangeError(ValueError): pass class NotIntegerError(ValueError): pass def toHexa(n): '''convert decimal to hexa''' if(n < 0): raise OutOfRangeError('number out of range, n < 0 )') if not isinstance(n, int): raise NotIntegerError('non-integers can not be converted') map = ('0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F') s = '' while(n/16): s += map[n % 16] n = n/16 s += map[n % 16] # reverse return s[::-1]
The hexatest3.py looks like this:
# hexatest3.py import hexa3 # This is the code being tested import unittest class Samples(unittest.TestCase): pair = ( (1,'1'), (2,'2'), (3,'3'), (4,'4'), (5,'5'), (6,'6'), (7,'7'), (8,'8'), (9,'9'), (10,'A'), (11,'B'), (12,'C'), (13,'D'), (14,'E'), (15,'F'), (16,'10'), (30,'1E'), (127,'7F'), (255,'FF'), (500,'1F4'), (1024,'400'), (5100,'13EC'), (65535,'FFFF'), (65536,'10000') ) def testToHexa(self): for d, h in self.pair: res = hexa3.toHexa(d); self.assertEqual(h, res) class BadSamples(unittest.TestCase): def testToHexaNegative(self): ''' toHex should fail when the input < 0''' self.assertRaises(hexa3.OutOfRangeError, hexa3.toHexa, -1) def testToHexaNonInteger(self): '''to_roman should fail with non-integer input''' self.assertRaises(hexa3.NotIntegerError, hexa3.toHexa, 0.5) if __name__ == '__main__': unittest.main()
Run the test case:
python hexatest3.py -v testToHexaNegative (__main__.BadSamples) toHex should fail when the input < 0 ... ok testToHexaNonInteger (__main__.BadSamples) to_roman should fail with non-integer input ... ok testToHexa (__main__.Samples) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.005s OK
OK. Passed!
Note: The basic idea of this chapter is borrowed from Dive into python3. Here, we used Hexa instead of Roman.
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization