Lesson 20 · Python standard library deep dive
Python unittest Explained: Write Tests That Catch Bugs | Standard Library #20
Video twenty of the twenty-five-part series: unittest, for writing and running real automated tests. TestCase, assertions, setUp/tearDown, exceptions,…
- CoursePython standard library deep dive
- Lesson20 of 24
- Video18 min
- FormatJupyter notebook · 11 code cells
What you'll learn
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbPython Standard Library Deep-Dive, Video 20: unittest#
- Video twenty of the twenty-five-part series: unittest, for writing and running real automated tests.
- TestCase, assertions, setUp/tearDown, exceptions, skipping, mocking, and subTest.
- Let's get into it.
Part 1: Why Automated Testing, and TestCase Basics#
import unittest
def add(a, b):
return a + b
class TestAdd(unittest.TestCase):
def test_add_positive(self):
self.assertEqual(add(2, 3), 5)
def test_add_negative(self):
self.assertEqual(add(-1, -1), -2)
suite = unittest.TestLoader().loadTestsFromTestCase(TestAdd)
result = unittest.TextTestRunner(verbosity=2).run(suite)
Part 2: Common Assert Methods#
class TestAssertions(unittest.TestCase):
def test_variety(self):
self.assertTrue(3 > 1)
self.assertFalse(1 > 3)
self.assertIn('a', ['a', 'b', 'c'])
self.assertNotIn('z', ['a', 'b', 'c'])
self.assertIsNone(None)
self.assertIsNotNone(42)
self.assertAlmostEqual(0.1 + 0.2, 0.3, places=9)
self.assertListEqual([1, 2, 3], [1, 2, 3])
suite = unittest.TestLoader().loadTestsFromTestCase(TestAssertions)
result = unittest.TextTestRunner(verbosity=2).run(suite)
Part 3: setUp and tearDown#
class TestStack(unittest.TestCase):
def setUp(self):
self.stack = []
print('setUp: fresh stack created')
def tearDown(self):
print(f'tearDown: stack had {len(self.stack)} items')
def test_push(self):
self.stack.append(1)
self.assertEqual(self.stack, [1])
def test_starts_empty(self):
self.assertEqual(self.stack, [])
suite = unittest.TestLoader().loadTestsFromTestCase(TestStack)
result = unittest.TextTestRunner(verbosity=2).run(suite)
Part 4: assertRaises - Testing Exceptions#
def divide(a, b):
if b == 0:
raise ValueError('cannot divide by zero')
return a / b
class TestDivide(unittest.TestCase):
def test_raises_on_zero(self):
with self.assertRaises(ValueError) as ctx:
divide(10, 0)
self.assertIn('zero', str(ctx.exception))
def test_normal_division(self):
self.assertEqual(divide(10, 2), 5)
suite = unittest.TestLoader().loadTestsFromTestCase(TestDivide)
result = unittest.TextTestRunner(verbosity=2).run(suite)
Part 5: Skipping Tests#
import sys
class TestSkipping(unittest.TestCase):
@unittest.skip('demonstrating unconditional skip')
def test_always_skipped(self):
self.fail('should never run')
@unittest.skipIf(sys.version_info < (3, 8), 'requires Python 3.8+')
def test_runs_on_modern_python(self):
self.assertTrue(True)
suite = unittest.TestLoader().loadTestsFromTestCase(TestSkipping)
result = unittest.TextTestRunner(verbosity=2).run(suite)
Part 6: Mocking Basics - unittest.mock#
from unittest.mock import Mock, patch
fake_service = Mock()
fake_service.get_user.return_value = {'id': 1, 'name': 'Ana'}
result = fake_service.get_user(1)
print(result)
fake_service.get_user.assert_called_once_with(1)
print('call verified')
import time
def slow_operation():
time.sleep(10)
return 'done'
class TestWithPatch(unittest.TestCase):
@patch('time.sleep')
def test_does_not_really_sleep(self, mock_sleep):
result = slow_operation()
self.assertEqual(result, 'done')
mock_sleep.assert_called_once_with(10)
suite = unittest.TestLoader().loadTestsFromTestCase(TestWithPatch)
result = unittest.TextTestRunner(verbosity=2).run(suite)
Part 7: setUpClass - Class-Level Fixtures#
class TestExpensiveSetup(unittest.TestCase):
@classmethod
def setUpClass(cls):
print('setUpClass: running once for the whole class')
cls.shared_data = list(range(1000))
def test_length(self):
self.assertEqual(len(self.shared_data), 1000)
def test_first_element(self):
self.assertEqual(self.shared_data[0], 0)
suite = unittest.TestLoader().loadTestsFromTestCase(TestExpensiveSetup)
result = unittest.TextTestRunner(verbosity=2).run(suite)
Part 8: subTest - Parametrized-Style Testing#
def is_even(n):
return n % 2 == 0
class TestIsEven(unittest.TestCase):
def test_multiple_values(self):
cases = [(2, True), (3, False), (4, True), (7, False)]
for value, expected in cases:
with self.subTest(value=value):
self.assertEqual(is_even(value), expected)
suite = unittest.TestLoader().loadTestsFromTestCase(TestIsEven)
result = unittest.TextTestRunner(verbosity=2).run(suite)
Part 9: Combining Multiple Test Cases into One Suite#
loader = unittest.TestLoader()
combined_suite = unittest.TestSuite()
combined_suite.addTests(loader.loadTestsFromTestCase(TestAdd))
combined_suite.addTests(loader.loadTestsFromTestCase(TestIsEven))
result = unittest.TextTestRunner(verbosity=1).run(combined_suite)
print(f'ran {result.testsRun} tests, {len(result.failures)} failures, {len(result.errors)} errors')
Part 10: A Real Pattern - Testing a Small Module End to End#
class ShoppingCart:
def __init__(self):
self.items = {}
def add_item(self, name, price, qty=1):
if price < 0:
raise ValueError('price cannot be negative')
self.items[name] = self.items.get(name, 0) + qty
return self.items[name]
def total(self, prices):
return sum(prices[name] * qty for name, qty in self.items.items())
class TestShoppingCart(unittest.TestCase):
def setUp(self):
self.cart = ShoppingCart()
self.prices = {'apple': 1.5, 'bread': 3.0}
def test_add_item(self):
self.cart.add_item('apple', 1.5, qty=3)
self.assertEqual(self.cart.items['apple'], 3)
def test_total(self):
self.cart.add_item('apple', 1.5, qty=2)
self.cart.add_item('bread', 3.0, qty=1)
self.assertAlmostEqual(self.cart.total(self.prices), 6.0)
def test_rejects_negative_price(self):
with self.assertRaises(ValueError):
self.cart.add_item('bad', -5)
def test_multiple_quantities(self):
for qty in [1, 2, 5, 10]:
with self.subTest(qty=qty):
cart = ShoppingCart()
cart.add_item('apple', 1.5, qty=qty)
self.assertEqual(cart.items['apple'], qty)
suite = unittest.TestLoader().loadTestsFromTestCase(TestShoppingCart)
result = unittest.TextTestRunner(verbosity=2).run(suite)
Wrap-Up: What You Learned#
- unittest.TestCase subclasses hold test_-prefixed methods, each treated as one independent test.
- assertEqual, assertTrue/False, assertIn/NotIn, assertIsNone, and assertAlmostEqual cover most common checks.
- setUp runs before every test, tearDown after every test, for fresh, isolated fixtures.
- assertRaises, used as a context manager, tests for exceptions and lets the caught exception be inspected.
- @unittest.skip and skipIf/skipUnless skip tests unconditionally or based on a runtime condition.
- unittest.mock.Mock and patch replace real dependencies with fakes, so tests avoid real network/filesystem calls.
- setUpClass runs once per class, for expensive, genuinely shared fixtures across every test in it.
- subTest runs a loop of related checks inside one test, reporting each sub-check's pass/fail independently.
- TestSuite combines multiple TestCase classes into one batch run, like discover does across a whole project.
- A real pattern: thoroughly testing a small module's normal logic, edge cases, and error handling together.
- That wraps up unittest. Next up: shutil, tempfile, and glob, for file system utilities.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



