🔄 Quick Recap (Day 23)
You mastered advanced functions: iterators, generators, and closures.
🎯 What You’ll Learn Today
unittest basics: structure, test cases, assertions, running tests.
pytest intro: simple tests,
pytest.raises, fixtures (tiny peek).Debugging strategies: reading tracebacks, breakpoints, stepping through code in the debugger, and smarter prints/logging.
📖 Why Test & Debug?
Testing prevents regressions and builds confidence to change code. Debugging skills help you pinpoint the exact line and value that caused a problem. Together they turn guesswork into a repeatable process.
📖 unittest: Batteries-Included Testing
unittest ships with Python and uses classes to group tests.
Example Code Under Test (calculator.py)
def add(a, b):
return a + b
def divide(a, b):
return a / bTest File (test_calculator_unittest.py)
import unittest
import calculator
class TestCalculator(unittest.TestCase):
def test_add(self):
self.assertEqual(calculator.add(2, 3), 5)
self.assertEqual(calculator.add(-1, 1), 0)
def test_divide(self):
self.assertAlmostEqual(calculator.divide(10, 2), 5.0)
def test_divide_by_zero(self):
with self.assertRaises(ZeroDivisionError):
calculator.divide(1, 0)
if __name__ == "__main__":
unittest.main()Run
python -m unittest -vExpected output (example):
test_add (test_calculator_unittest.TestCalculator) ... ok
test_divide (test_calculator_unittest.TestCalculator) ... ok
test_divide_by_zero (test_calculator_unittest.TestCalculator) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.00s
OK📖 pytest: Short, Sweet Tests
pytest uses plain functions and simple assert statements.
Install
pip install pytestTest File (test_calculator_pytest.py)
import pytest
from calculator import add, divide
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
def test_divide():
assert divide(10, 2) == 5.0
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(1, 0)Run
pytest -qExpected output (example):
3 passedTiny peek at fixtures: you can define a @pytest.fixture to create test data once and reuse it in multiple tests.
📖 Debugging Strategies that Work
Read the traceback carefully: note the file, line number, and exact error type.
Reproduce consistently: same inputs, same environment.
Narrow it down: binary search with prints/logs or step through with a debugger.
Use breakpoints: pause execution right before the suspected bug.
Inspect variables: check actual runtime values, not what you expect.
Fix one thing at a time: then re-run tests.
Using the Built-in Debugger
Insert a breakpoint where you want to pause:
breakpoint() # Python 3.7+Common commands in the debugger:
n(next): step to the next line in the same function.s(step): step into a function call.c(continue): run until next breakpoint/exceptions.l(list): show source around the current line.p var(print): show the value ofvar.
Smart Prints vs Logging
For quick checks, prints are fine.
For real projects, prefer logging with levels (
DEBUG,INFO,WARNING,ERROR) so you can turn verbosity up or down without changing code behavior.
🧙♂️ Take the Wand and Try Yourself
Goal: Write tests that catch a subtle bug, debug it with a breakpoint, then fix it.
Create
stats.pywith a deliberately buggy function:
def average(nums):
# BUG: dividing by fixed 2 instead of length
return sum(nums) / 2Write unittest tests in
test_stats_unittest.py:
import unittest
from stats import average
class TestStats(unittest.TestCase):
def test_average_basic(self):
self.assertEqual(average([10, 20]), 15)
self.assertEqual(average([1, 2, 3]), 2)
if __name__ == "__main__":
unittest.main()Run:
python -m unittest -vYou should see a failure for the [1,2,3] case (actual 3.0 vs expected 2).
Debug with a breakpoint inside
average:
def average(nums):
breakpoint()
return sum(nums) / 2Run the failing test and at the (Pdb) prompt:
p nums→ confirm it’s[1, 2, 3]p len(nums)→ see it’s3Realize the divisor should be
len(nums).
Fix the bug:
def average(nums):
return sum(nums) / len(nums)Re-run tests → all green.
Expected output (after fix):
test_average_basic (test_stats_unittest.TestStats) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.00s
OK(Optional) pytest version
test_stats_pytest.py:
from stats import average
def test_average_basic():
assert average([10, 20]) == 15
assert average([1, 2, 3]) == 2Run:
pytest -qExpected: 1 passed (or 2 passed if both asserts are counted separately by your pytest version).
Once you see the failing test turn green after the fix—and you’ve stepped through with a breakpoint—you’ve got the core testing & debugging loop down!
Up next: Day 25: Packaging & Distribution — build, version, and publish installable Python packages.