How do you know that your function or class behaves correctly? One very natural way to check for correct behavior is to try a few test cases, and compare the result to your expectation. You've been informally doing this throughout this course. In these lecture notes, we'll discuss a formal and automated way to approach testing your code. This is called unit testing, and refers to the practice of checking that each function and class (i.e. each unit of your program) behaves as you expect. Almost all production code is extensively unit-tested, and often the "actual" code is dwarfed by the amount of code used to implement unit tests.
For a review of the informal approach to unit testing, let's look at the following function. This function conducts a reverse lookup in a dictionary: that is, it finds all the keys corresponding to a user-specified value.
def reverse_lookup(D, val):
"""
Finds all keys in a dictionary D with specified value val.
"""
if type(D) != dict:
raise TypeError("First argument must be a dictionary.")
return [key for key in D.keys() if D[key] == val]
We can try a few examples to check that the behavior is as we would expect:
D = ["Riker", "Picard", "Troi"]
reverse_lookup(D, "Picard")
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-2-74937265f2aa> in <module> 1 D = ["Riker", "Picard", "Troi"] ----> 2 reverse_lookup(D, "Picard") <ipython-input-1-8d7aa0acc961> in reverse_lookup(D, val) 4 """ 5 if type(D) != dict: ----> 6 raise TypeError("First argument must be a dictionary.") 7 8 return [key for key in D.keys() if D[key] == val] TypeError: First argument must be a dictionary.
D = {
"Riker" : "TNG",
"Picard" : "TNG",
"Sisko" : "DS9"
}
len(reverse_lookup(D, "TNG")) == 2
True
len(reverse_lookup(D, "TOS")) == 0
True
This works fine for simple functions, but more complex functions or classes might have tens or even hundreds of tests. We wouldn't want to re-run all those tests by hand when we change the code (protip: code is never finalized).
Let's see how to automate these test cases using the unittest
module. To organize and automate our testing, we are going to write a custom class. Each method of the class corresponds to one or more of these test cases. This class should inherit from the special class unittest.TestCase
. When the nature of the tests is sufficiently obvious, it is not necessary to supply docstrings.
import unittest
unittest
module¶To organize and automate our testing, we are going to write a custom class. Each method of the class corresponds to one or more of these test cases. This class should inherit from the special class unittest.TestCase
. When the nature of the tests is sufficiently obvious, it is not necessary to supply docstrings.
class TestReverseLookup(unittest.TestCase):
def test_type_error(self):
D = ["Riker", "Picard", "Sisko"]
with self.assertRaises(TypeError):
reverse_lookup(D, "Picard")
def test_standard_lookup(self):
D = {
"Riker" : "TNG",
"Picard" : "TNG",
"Sisko" : "DS9"
}
result = len(reverse_lookup(D, "TNG"))
self.assertEqual(result, 2)
def test_no_matches(self):
D = {
"Riker" : "TNG",
"Picard" : "TNG",
"Sisko" : "DS9"
}
result = len(reverse_lookup(D, "DS9"))
self.assertEqual(result, 0)
When the tests pass, nothing happens.
tester = TestReverseLookup()
tester.test_type_error()
tester.test_standard_lookup()
On the other hand, when a test fails, an assertionError
is raised.
tester.test_no_matches()
# ---
--------------------------------------------------------------------------- AssertionError Traceback (most recent call last) <ipython-input-8-f255e0a40e98> in <module> ----> 1 tester.test_no_matches() 2 # --- <ipython-input-6-10d94f8896f5> in test_no_matches(self) 23 24 result = len(reverse_lookup(D, "DS9")) ---> 25 self.assertEqual(result, 0) /opt/anaconda3/lib/python3.8/unittest/case.py in assertEqual(self, first, second, msg) 910 """ 911 assertion_func = self._getAssertEqualityFunc(first, second) --> 912 assertion_func(first, second, msg=msg) 913 914 def assertNotEqual(self, first, second, msg=None): /opt/anaconda3/lib/python3.8/unittest/case.py in _baseAssertEqual(self, first, second, msg) 903 standardMsg = '%s != %s' % _common_shorten_repr(first, second) 904 msg = self._formatMessage(msg, standardMsg) --> 905 raise self.failureException(msg) 906 907 def assertEqual(self, first, second, msg=None): AssertionError: 1 != 0
While this test setup works well, it's cumbersome -- you need to call and run the tests by hand each time. The standard and much more convenient approach is to embed your tests into the module in which you define your classes and functions. An example of this approach is shown in the accompanying file unit_test_example.py
. Download this file here. The key trick is in the following two lines:
if __name__ == "__main__":
unittest.main()
The unittest.main()
method will find all classes that inherit from unittest.TestCase
, construct an instance of each class, and then run each method of each class exactly once, with custom exception handling to ensure that all tests run even if some of them produce AssertionErrors
. It will then give a summary of the number of failures and the time it took to run the tests. The first line ensures that the unit tests are performed only when running the module as a script, and not when importing the module.
import unit_test_example # tests not run
In PIC16A, you are not generally required to include unit tests with your code. However, you are responsible for understanding how to construct them, and may be asked to do so in assignments and exams.