Testing in Python — The Complete Notebook
Comprehensive guide on Testing in Python — The Complete Notebook.
Testing in Python
1. Overview#
Tests are automated checks that verify your code behaves correctly — catching regressions before they reach production. Python's standard library ships unittest, but the ecosystem has largely standardized on pytest for its simpler syntax and powerful fixture system.
2. Getting Started with pytest#
2.1 Installation & Basic Test#
bashpip install pytest
🐍 PythonInteractive WebAssembly# file: calculator.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
🐍 PythonInteractive WebAssembly# file: test_calculator.py
from calculator import add, divide
def test_add():
assert add(2, 3) == 5
def test_add_negative_numbers():
assert add(-1, -1) == -2
def test_divide_by_zero_raises():
import pytest
with pytest.raises(ValueError):
divide(10, 0)
bashpytest # run all tests in the current directory
pytest test_calculator.py # run a specific file
pytest -v # verbose output
pytest -k "add" # run only tests matching "add"
pytestauto-discovers tests: files namedtest_*.pyor*_test.py, functions namedtest_*, inside classes namedTest*. No boilerplate class inheritance required, unlikeunittest.
3. Assertions#
pytest uses plain assert statements and rewrites them to give detailed failure output automatically.
🐍 PythonInteractive WebAssemblydef test_examples():
assert 2 + 2 == 4
assert "hello".upper() == "HELLO"
assert [1, 2, 3] == [1, 2, 3]
assert 5 in [1, 2, 3, 4, 5]
assert isinstance(5, int)
When a test fails, pytest shows exactly what was compared:
codedef test_add(): > assert add(2, 3) == 6 E assert 5 == 6
4. Fixtures — Reusable Test Setup#
A fixture provides data or a resource that tests need, with automatic setup and teardown.
🐍 PythonInteractive WebAssemblyimport pytest
@pytest.fixture
def sample_users():
return [
{"name": "Asha", "age": 29},
{"name": "Ravi", "age": 34},
]
def test_user_count(sample_users):
assert len(sample_users) == 2
def test_first_user_name(sample_users):
assert sample_users[0]["name"] == "Asha"
4.1 Fixtures with Teardown (Setup/Cleanup)#
🐍 PythonInteractive WebAssemblyimport pytest
@pytest.fixture
def database_connection():
conn = connect_to_test_db() # setup
yield conn # test runs here, using `conn`
conn.close() # teardown — runs after the test finishes
def test_query(database_connection):
result = database_connection.execute("SELECT 1")
assert result is not None
4.2 Fixture Scope#
🐍 PythonInteractive WebAssembly@pytest.fixture(scope="function") # default — runs fresh for every test
def per_test_fixture():
...
@pytest.fixture(scope="module") # runs once per test file
def per_module_fixture():
...
@pytest.fixture(scope="session") # runs once for the entire test run
def per_session_fixture():
...
5. Parametrized Tests#
Run the same test logic against multiple inputs without duplicating code.
🐍 PythonInteractive WebAssemblyimport pytest
from calculator import add
@pytest.mark.parametrize("a, b, expected", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
(100, 200, 300),
])
def test_add_parametrized(a, b, expected):
assert add(a, b) == expected
6. Mocking — Isolating Code from External Dependencies#
Mocking replaces a real dependency (API call, database, file system) with a fake, controllable stand-in — so tests stay fast and don't depend on external systems.
🐍 PythonInteractive WebAssemblyfrom unittest.mock import Mock, patch
def get_weather(api_client, city):
response = api_client.fetch(city)
return response["temperature"]
def test_get_weather():
mock_client = Mock()
mock_client.fetch.return_value = {"temperature": 28}
result = get_weather(mock_client, "Bengaluru")
assert result == 28
mock_client.fetch.assert_called_once_with("Bengaluru")
6.1 patch — Replacing a Real Function/Module Temporarily#
🐍 PythonInteractive WebAssembly# file: weather_service.py
import requests
def get_temperature(city):
response = requests.get(f"https://api.weather.com/{city}")
return response.json()["temperature"]
🐍 PythonInteractive WebAssembly# file: test_weather_service.py
from unittest.mock import patch
from weather_service import get_temperature
@patch("weather_service.requests.get") # patches requests.get only inside this module
def test_get_temperature(mock_get):
mock_get.return_value.json.return_value = {"temperature": 30}
result = get_temperature("Bengaluru")
assert result == 30
Always patch where the name is used, not where it's originally defined —
"weather_service.requests.get", not"requests.get".
7. Testing Exceptions#
🐍 PythonInteractive WebAssemblyimport pytest
from calculator import divide
def test_divide_by_zero():
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)
8. Organizing a Test Suite#
codemy_project/ ├── src/ │ └── calculator.py ├── tests/ │ ├── conftest.py # shared fixtures, auto-discovered by pytest │ ├── test_calculator.py │ └── test_weather.py └── pytest.ini # pytest configuration
🐍 PythonInteractive WebAssembly# conftest.py — fixtures here are available to ALL test files automatically
import pytest
@pytest.fixture
def sample_data():
return {"key": "value"}
ini# pytest.ini
[pytest]
testpaths = tests
addopts = -v --strict-markers
9. Measuring Coverage#
bashpip install pytest-cov pytest --cov=src --cov-report=term-missing
This reports what percentage of your source code is actually exercised by tests, and flags exactly which lines were missed.
10. Common Pitfalls#
INCORRECT: Tests That Depend on Each Other's Order#
🐍 PythonInteractive WebAssemblycounter = 0
def test_increment():
global counter
counter += 1
assert counter == 1 # breaks if this test doesn't run first
CORRECT: Fix — Each Test Should Be Independent#
Use fixtures to reset state before every test instead of relying on shared globals or execution order.
INCORRECT: Testing Implementation Details Instead of Behavior#
🐍 PythonInteractive WebAssemblydef test_internal_list_used():
calc = Calculator()
calc.add(2, 3)
assert calc._history == [("add", 2, 3)] # brittle — breaks on refactor
CORRECT: Fix — Test the Public Behavior#
🐍 PythonInteractive WebAssemblydef test_add_returns_correct_result():
calc = Calculator()
assert calc.add(2, 3) == 5
11. Summary & Best Practices Checklist#
- Name test files
test_*.pyand test functionstest_*sopytestauto-discovers them. - Keep each test independent — no shared mutable state between tests.
- Use fixtures for setup/teardown instead of repeating code in every test.
- Use
@pytest.mark.parametrizeinstead of copy-pasting near-identical tests. - Mock external dependencies (APIs, databases, file systems) — tests should be fast and deterministic.
- Patch names where they're used, not where they're defined.
- Test behavior/outputs, not internal implementation details.
- Track coverage, but don't chase 100% blindly — focus on meaningful paths and edge cases.
Testing in Python & Pytest Checkpoint
Finished studying this notebook?
Mark this guide as completed to update your course progress roadmap.