🔄 Quick Recap (Day 23)

  • You mastered advanced functions: iterators, generators, and closures.

🎯 What You’ll Learn Today

  1. unittest basics: structure, test cases, assertions, running tests.

  2. pytest intro: simple tests, pytest.raises, fixtures (tiny peek).

  3. 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 / b

Test 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 -v

Expected 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 pytest

Test 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 -q

Expected output (example):

3 passed

Tiny peek at fixtures: you can define a @pytest.fixture to create test data once and reuse it in multiple tests.

📖 Debugging Strategies that Work

  1. Read the traceback carefully: note the file, line number, and exact error type.

  2. Reproduce consistently: same inputs, same environment.

  3. Narrow it down: binary search with prints/logs or step through with a debugger.

  4. Use breakpoints: pause execution right before the suspected bug.

  5. Inspect variables: check actual runtime values, not what you expect.

  6. 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 of var.

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.

  1. Create stats.py with a deliberately buggy function:

def average(nums):
    # BUG: dividing by fixed 2 instead of length
    return sum(nums) / 2
  1. Write 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 -v

You should see a failure for the [1,2,3] case (actual 3.0 vs expected 2).

  1. Debug with a breakpoint inside average:

def average(nums):
    breakpoint()
    return sum(nums) / 2

Run the failing test and at the (Pdb) prompt:

  • p nums → confirm it’s [1, 2, 3]

  • p len(nums) → see it’s 3

  • Realize the divisor should be len(nums).

  1. 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
  1. (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]) == 2

Run:

pytest -q

Expected: 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.

Keep Reading