Python Unit Testing — pytest & Test-Driven Development
Testing ensures your code works correctly and continues to work as you make changes.
Learning Objectives
- Write tests with pytest
- Use fixtures for test setup
- Parametrize tests for multiple inputs
- Mock external dependencies
pytest Basics
def add(a, b):
return a + b
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, -1) == -2
def test_add_zero():
assert add(0, 5) == 5
# Run: pytest test_calculator.py
Fixtures
import pytest
@pytest.fixture
def sample_data():
return {"users": ["Alice", "Bob"], "count": 2}
@pytest.fixture
def db_connection():
conn = create_connection()
yield conn
conn.close()
def test_user_count(sample_data):
assert sample_data["count"] == len(sample_data["users"])
def test_database(db_connection):
db_connection.execute("SELECT 1")
Parametrize
@pytest.mark.parametrize("input,expected", [
(1, 2),
(2, 4),
(3, 6),
(0, 0),
(-1, -2),
])
def test_double(input, expected):
assert double(input) == expected
Mocking
from unittest.mock import patch, MagicMock
def get_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
@patch('requests.get')
def test_get_user(mock_get):
mock_get.return_value.json.return_value = {"name": "Alice"}
result = get_user_data(1)
assert result["name"] == "Alice"
mock_get.assert_called_once()
Test Organization
# conftest.py — shared fixtures
@pytest.fixture
def api_client():
return TestClient(app)
# test_api.py
class TestUserAPI:
def test_create_user(self, api_client):
response = api_client.post("/users", json={"name": "Alice"})
assert response.status_code == 201
def test_get_user(self, api_client):
response = api_client.get("/users/1")
assert response.status_code == 200
Key Takeaways
- Name test files
test_*.pyand functionstest_* - Use fixtures for reusable setup/teardown
@pytest.mark.parametrizefor data-driven tests- Mock external dependencies (APIs, databases)
- Run tests with
pytest -vfor detailed output