If you have heard of “Test-Driven Development (TDD)”, unit testing will be familiar to you.
Unit testing is a type of testing work used to verify the correctness of a module, a function, or a class.
For example, for the abs() function, we can write the following test cases:
TypeError to be raised.Putting the above test cases into a test module forms a complete unit test.
If the unit test passes, it means the function we tested works correctly. If the unit test fails, either the function has a bug or the test input conditions are incorrect—in any case, we need to fix the issue to make the unit test pass.
What is the significance of passing unit tests? If we modify the code of the abs() function, we only need to run the unit tests again. If they pass, it means our modification does not affect the original behavior of the abs() function. If the tests fail, it means our modification is inconsistent with the original behavior—we either need to modify the code or adjust the test cases.
The biggest advantage of this test-driven development model is ensuring that the behavior of a program module conforms to our designed test cases. When making modifications in the future, it can largely guarantee that the module’s behavior remains correct.
Let’s write a Dict class that behaves like a built-in dict but allows attribute-based access, used as follows:
>>> d = Dict(a=1, b=2)
>>> d['a']
1
>>> d.a
1
The code for mydict.py is as follows:
class Dict(dict):
def __init__(self, **kw):
super().__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
To write unit tests, we need to import Python’s built-in unittest module. Create mydict_test.py as follows:
import unittest
from mydict import Dict
class TestDict(unittest.TestCase):
def test_init(self):
d = Dict(a=1, b='test')
self.assertEqual(d.a, 1)
self.assertEqual(d.b, 'test')
self.assertTrue(isinstance(d, dict))
def test_key(self):
d = Dict()
d['key'] = 'value'
self.assertEqual(d.key, 'value')
def test_attr(self):
d = Dict()
d.key = 'value'
self.assertTrue('key' in d)
self.assertEqual(d['key'], 'value')
def test_keyerror(self):
d = Dict()
with self.assertRaises(KeyError):
value = d['empty']
def test_attrerror(self):
d = Dict()
with self.assertRaises(AttributeError):
value = d.empty
When writing unit tests, we need to create a test class that inherits from unittest.TestCase.
Methods starting with test are test methods; methods not prefixed with test are not recognized as test methods and will not be executed during testing.
We need to write a test_xxx() method for each category of tests. Since unittest.TestCase provides many built-in condition checks, we only need to call these methods to assert whether the output matches our expectations. The most commonly used assertion is assertEqual():
self.assertEqual(abs(-1), 1) # Assert the function's return value equals 1
Another important assertion is expecting a specific type of Error to be raised. For example, when accessing a non-existent key via d['empty'], we assert that a KeyError will be raised:
with self.assertRaises(KeyError):
value = d['empty']
When accessing a non-existent key via d.empty, we expect an AttributeError to be raised:
with self.assertRaises(AttributeError):
value = d.empty
Once the unit tests are written, we can run them. The simplest way is to add two lines of code at the end of mydict_test.py:
if __name__ == '__main__':
unittest.main()
This allows mydict_test.py to be run as a regular Python script:
$ python mydict_test.py
An alternative method is to run unit tests directly from the command line using the -m unittest argument:
$ python -m unittest mydict_test
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s
OK
This is the recommended approach because it enables batch execution of multiple unit tests at once, and many tools can automatically run these tests.
During development, we often want to repeatedly execute a single test method (e.g., test_attr()) instead of running all test methods every time. We can specify a single test method using the syntax module.class.method:
$ python -m unittest mydict_test.TestDict.test_attr
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Here:
module = the filename mydict_test (without .py)class = the test class TestDictmethod = the specified test method name test_attrTo execute two test methods (test_attr() and test_attrerror()), we can use the -k parameter to match tests containing attr:
$ python -m unittest mydict_test -k attr -v
test_attr (mydict_test.TestDict.test_attr) ... ok
test_attrerror (mydict_test.TestDict.test_attrerror) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
In the command above:
-v parameter prints the specific test methods being executed-k attr parameter filters test methods containing attrThis demonstrates the flexibility of unit test execution in Python.
We can write two special methods in unit tests: setUp() and tearDown(). These methods are executed before and after each test method is called, respectively.
What are setUp() and tearDown() used for? Imagine your tests require a database connection: you can connect to the database in setUp() and close it in tearDown(), eliminating the need to repeat this code in every test method:
class TestDict(unittest.TestCase):
def setUp(self):
print('setUp...')
def tearDown(self):
print('tearDown...')
Rerun the tests to verify that setUp... and tearDown... are printed before and after each test method is invoked.
Write unit tests for the Student class—you will find the tests fail. Modify the Student class to make the tests pass:
import unittest
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def get_grade(self):
if self.score >= 60:
return 'B'
if self.score >= 80:
return 'A'
return 'C'
class TestStudent(unittest.TestCase):
def test_80_to_100(self):
s1 = Student('Bart', 80)
s2 = Student('Lisa', 100)
self.assertEqual(s1.get_grade(), 'A')
self.assertEqual(s2.get_grade(), 'A')
def test_60_to_80(self):
s1 = Student('Bart', 60)
s2 = Student('Lisa', 79)
self.assertEqual(s1.get_grade(), 'B')
self.assertEqual(s2.get_grade(), 'B')
def test_0_to_60(self):
s1 = Student('Bart', 0)
s2 = Student('Lisa', 59)
self.assertEqual(s1.get_grade(), 'C')
self.assertEqual(s2.get_grade(), 'C')
def test_invalid(self):
s1 = Student('Bart', -1)
s2 = Student('Lisa', 101)
with self.assertRaises(ValueError):
s1.get_grade()
with self.assertRaises(ValueError):
s2.get_grade()
if __name__ == '__main__':
unittest.main()
Unit tests effectively verify the behavior of individual program modules and provide confidence for future code refactoring.
Test cases for unit tests should cover:
Unit test code should be extremely simple—if test code is overly complex, it may contain bugs itself.
Passing unit tests does not guarantee a program is bug-free, but failing unit tests definitely indicate the presence of bugs.
class Dict(dict):
def __init__(self, **kw):
super().__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import unittest
from mydict import Dict
class TestDict(unittest.TestCase):
def test_init(self):
d = Dict(a=1, b="test")
self.assertEqual(d.a, 1)
self.assertEqual(d.b, "test")
self.assertTrue(isinstance(d, dict))
def test_key(self):
d = Dict()
d["key"] = "value"
self.assertEqual(d.key, "value")
def test_attr(self):
d = Dict()
d.key = "value"
self.assertTrue("key" in d)
self.assertEqual(d["key"], "value")
def test_keyerror(self):
d = Dict()
with self.assertRaises(KeyError):
value = d["empty"]
def test_attrerror(self):
d = Dict()
with self.assertRaises(AttributeError):
value = d.empty
if __name__ == "__main__":
unittest.main()