Testing in Python
Comprehensive guide on Testing in Python.
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
Python# 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
Python# 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"
pytest auto-discovers tests: files named test_*.py or *_test.py, functions named test_*, inside classes named Test*. No boilerplate class inheritance required, unlike unittest.
3. Assertions#
pytest uses plain assert statements and rewrites them to give detailed failure output automatically.
Pythondef 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.
Pythonimport 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)#
Pythonimport 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#
Python@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.
Pythonimport 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.
Pythonfrom 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#
Python# file: weather_service.py
import requests
def get_temperature(city):
response = requests.get(f"https://api.weather.com/{city}")
return response.json()["temperature"]
Python# 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#
Pythonimport 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#
Architecture & Data Flowmy_project/ ├── src/ │ └── calculator.py ├── tests/ │ ├── conftest.py # shared fixtures, auto-discovered by pytest │ ├── test_calculator.py │ └── test_weather.py └── pytest.ini # pytest configuration
Python# 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#
Pythoncounter = 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#
Pythondef 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#
Pythondef 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.