Master Python unittest: From Basics to Advanced Techniques
This guide walks through Python's unittest module, covering fundamental concepts, test suites, loaders, custom result handling, skipping tests, retry decorators, execution order, tagging, class-level setup/teardown, and exception‑message assertions, each illustrated with clear code examples.
1. unittest Basics
The unittest module provides a structured way to write test cases by inheriting from unittest.TestCase and defining methods that start with test_.
2. TestSuite
unittest.TestSuiteacts as a container for multiple test cases, allowing batch execution.
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
class TestNumberMethods(unittest.TestCase):
def test_add(self):
self.assertEqual(1 + 2, 3)
def suite():
suite = unittest.TestSuite()
suite.addTest(TestStringMethods('test_upper'))
suite.addTest(TestNumberMethods('test_add'))
return suite
if __name__ == '__main__':
runner = unittest.TextTestRunner()
runner.run(suite())3. TestLoader
unittest.TestLoadercan automatically discover and load test cases.
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
class TestNumberMethods(unittest.TestCase):
def test_add(self):
self.assertEqual(1 + 2, 3)
if __name__ == '__main__':
loader = unittest.TestLoader()
tests = loader.discover(start_dir='.', pattern='test*.py')
runner = unittest.TextTestRunner()
runner.run(tests)4. Custom TestResult
By subclassing unittest.TextTestResult, you can customize how test results are displayed.
import unittest
class CustomTestResult(unittest.TextTestResult):
def addSuccess(self, test):
super().addSuccess(test)
print(f"Test succeeded: {test}")
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
if __name__ == '__main__':
loader = unittest.TestLoader()
tests = loader.loadTestsFromTestCase(TestStringMethods)
runner = unittest.TextTestRunner(resultclass=CustomTestResult)
runner.run(tests)5. Skipping Tests
Use @unittest.skip or @unittest.skipIf decorators to omit tests conditionally.
import unittest
class TestStringMethods(unittest.TestCase):
@unittest.skip("temporarily skip this test")
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
if __name__ == '__main__':
unittest.main()
# Conditional skip example
class TestStringMethods(unittest.TestCase):
@unittest.skipIf(True, "skip when condition is true")
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')6. Retry Decorator
Although unittest lacks built‑in retry, a custom decorator can re‑run flaky tests.
import unittest, time
def retry(max_attempts=3, delay=1):
def decorator(func):
def wrapper(*args, **kwargs):
attempts = 0
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
if attempts >= max_attempts:
raise
time.sleep(delay)
return wrapper
return decorator
class TestStringMethods(unittest.TestCase):
@retry(max_attempts=3, delay=1)
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
if __name__ == '__main__':
unittest.main()7. Controlling Test Order
By default, tests run in definition order; you can change it by customizing the loader.
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
def test_isupper(self):
self.assertTrue('FOO'.isupper())
if __name__ == '__main__':
loader = unittest.TestLoader()
loader.sortTestMethodsUsing = lambda _, x, y: -1 if x > y else 1
tests = loader.loadTestsFromTestCase(TestStringMethods)
runner = unittest.TextTestRunner()
runner.run(tests)8. Adding Tags via Custom Loader
unittest doesn't support tags directly, but you can filter tests in a custom loader.
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
def load_tests(loader, tests, pattern):
suite = unittest.TestSuite()
for test_case in tests:
if hasattr(test_case, '_testMethodName'):
if 'slow' in getattr(test_case, '_testMethodName'):
continue
suite.addTest(test_case)
return suite
if __name__ == '__main__':
loader = unittest.TestLoader()
tests = loader.loadTestsFromTestCase(TestStringMethods)
tests = load_tests(loader, tests, pattern='test*')
runner = unittest.TextTestRunner()
runner.run(tests)9. setUpClass and tearDownClass
Class‑level setup/teardown run once before/after all tests in the class.
import unittest
class TestStringMethods(unittest.TestCase):
@classmethod
def setUpClass(cls):
print("setting up test environment")
@classmethod
def tearDownClass(cls):
print("cleaning up test environment")
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
if __name__ == '__main__':
unittest.main()10. assertRaisesRegex
Use assertRaisesRegex to verify that an exception contains a specific message.
import unittest
class TestStringMethods(unittest.TestCase):
def test_split(self):
s = 'hello world'
with self.assertRaisesRegex(ValueError, "empty separator"):
s.split('')
if __name__ == '__main__':
unittest.main()Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
