Intermediate
12 min read
#Python#Testing#pytest#Mocking#TDD

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#

bash
pip 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)
bash
pytest # 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.

🐍 Python
def 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:

code
def 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.

🐍 Python
import 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)#

🐍 Python
import 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.

🐍 Python
import 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.

🐍 Python
from 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#

🐍 Python
import 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#

code
my_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#

bash
pip 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#

🐍 Python
counter = 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#

🐍 Python
def 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#

🐍 Python
def test_add_returns_correct_result(): calc = Calculator() assert calc.add(2, 3) == 5

11. Summary & Best Practices Checklist#

  • Name test files test_*.py and test functions test_* so pytest auto-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.parametrize instead 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.
Knowledge Checkpoint

Testing in Python & Pytest Checkpoint

Q1.What is a Pytest `fixture` used for?
ATo compile Python tests into binary executables.
BTo provide reusable setup and teardown logic, test data, or mock dependencies across test functions.
CTo measure line coverage automatically.
DTo generate randomized passwords for tests.
Q2.Which Pytest decorator allows running a single test function across multiple parameterized inputs and expected outputs?
A@pytest.mark.parametrize
B@pytest.mark.repeat
C@pytest.mark.loop
D@pytest.mark.cases
Q3.What is the primary purpose of `unittest.mock.patch` in unit testing?
ATo replace external dependencies (like HTTP requests or third-party APIs) with controllable mock objects during tests.
BTo patch Python security vulnerabilities.
CTo speed up disk I/O.
DTo automatically fix failing assertions.
Track Your Learning

Finished studying this notebook?

Mark this guide as completed to update your course progress roadmap.