Testing & Debugging in Python (unittest, pytest, pdb)
Testing ensures your code does what you think it does; debugging helps you figure out why it doesn’t. You’ll use:
unittest: built-in xUnit-style framework.pytest: popular, concise, powerful.pdb: interactive debugger (alsobreakpoint()).
`unittest` (Standard Library)
Basic structure
project/
└─ src/
└─ mathy.py
└─ tests/
└─ test_mathy_unittest.py
# src/mathy.py
def add(a, b): return a + b
def div(a, b): return a / b
# tests/test_mathy_unittest.py
import unittest
from src.mathy import add, div
class TestMathy(unittest.TestCase):
def test_add(self):
self.assertEqual(add(2, 3), 5)
def test_div_by_zero(self):
with self.assertRaises(ZeroDivisionError):
div(1, 0)
if __name__ == "__main__":
unittest.main()
Run: python -m unittest -v
Handy asserts: assertEqual, assertTrue/False, assertIsNone, assertIn, assertAlmostEqual, assertRaises.
Fixtures: override setUp, tearDown, or class-level setUpClass, tearDownClass.
`pytest` (Concise & Powerful)
Install: python -m pip install pytest
Conventions
- Files named
test_.pyor_test.py - Test functions:
def test_...(): - Simple
assertstatements.
# tests/test_mathy_pytest.py
from src.mathy import add, div
import pytest
def test_add():
assert add(2, 3) == 5
def test_div_by_zero():
with pytest.raises(ZeroDivisionError):
div(1, 0)
Run: pytest -q
Useful flags: -vv verbose, -k expr filter, -x stop on first fail, -s show print output, --maxfail=1.
Parametrization
import pytest
from src.mathy import add
@pytest.mark.parametrize("a,b,res", [(1,2,3),(0,0,0),(-1,1,0)])
def test_add_param(a,b,res):
assert add(a,b) == res
Fixtures (setup/teardown without classes)
# tests/conftest.py
import tempfile, shutil, pytest, pathlib
@pytest.fixture
def tempdir():
d = pathlib.Path(tempfile.mkdtemp())
yield d
shutil.rmtree(d)
# tests/test_files.py
def test_writes_file(tempdir):
p = tempdir / "out.txt"
p.write_text("hi")
assert p.read_text() == "hi"
Built-ins you’ll love: tmp_path, monkeypatch, capsys (capture stdout/stderr).
Mocking
pytest plays well with stdlib unittest.mock:
from unittest.mock import patch
from src.mathy import div
@patch("src.mathy.div", return_value=10)
def test_stubbed_div(mock_div):
assert div(100, 5) == 10
mock_div.assert_called_once()
Or patch env/IO with monkeypatch:
def test_env(monkeypatch):
monkeypatch.setenv("API_TOKEN", "test")
Configuration
Create pytest.ini:
[pytest]
addopts = -vv --strict-markers
testpaths = tests
Coverage
python -m pip install pytest-cov
pytest --cov=src --cov-report=term-missing
Debugging with `pdb` (and `breakpoint()`)
Quick use
# anywhere in code or tests
breakpoint() # Python 3.7+: enters debugger (pdb by default)
Run your script/tests, execution stops at the breakpoint.
Common commands
nnext linesstep intoccontinuellist sourcep exprprintpp exprpretty-printu/dup/down stackbtbacktraceqquit
Drop into pdb on failure
pytest -x --pdb→ stop at first failure in debugger
- Or post-mortem:
import pdb, traceback, sys
try:
risky()
except Exception:
traceback.print_exc()
pdb.post_mortem()
Use another debugger (optional)
Set PYTHONBREAKPOINT=ipdb.set_trace if using ipdb.
Choosing between `unittest` and `pytest`
- Start with
pytestfor most projects: fewer lines, rich ecosystem. - Use
unittestif you must stay stdlib-only or integrate with legacy test suites. - You can mix them:
pytestcan rununittesttests.
Test structure (suggested)
project/
├─ src/
│ └─ ...
├─ tests/
│ ├─ conftest.py # shared fixtures
│ ├─ test_api.py
│ ├─ test_mathy.py
│ └─ test_integration.py
├─ requirements.txt
└─ pytest.ini
Types of tests
- Unit: small, isolated (fast, mocked I/O).
- Integration: components together (DB/API).
- End-to-end: full flow (slow, few).
Aim for fast, deterministic unit tests; run them on every change.
Quick patterns & tips
- AAA pattern: Arrange, Act, Assert.
- Name tests by behavior:
test_add_handles_negatives. - Small steps: one assertion focus; failure message should be obvious.
- Use factories/fixtures to avoid repetitive setup.
- Mock boundaries (network, filesystem, time).
- Randomness/time: seed PRNG, freeze/fake time where needed.
- Flaky tests: remove nondeterminism; as last resort mark
@pytest.mark.flaky.
Mini “cheat sheet”
# unittest
python -m unittest -v
# pytest basics
pytest -q
pytest -vv -k add # run tests matching 'add'
pytest -x # stop on first failure
pytest --maxfail=1 --pdb # drop into debugger on failure
pytest --cov=src --cov-report=term-missing
# run a single test file / node
pytest tests/test_mathy_pytest.py::test_add