Why pytest?
pytest is the most popular testing framework in the Python ecosystem. It offers a simple syntax, powerful fixtures, informative error messages, and a rich plugin ecosystem. It can run tests written for unittest and nose as well, making migration easy.
Writing Your First Tests
# Install pytest
# pip install pytest
# test_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
# Tests - just use assert!
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(0, 0) == 0
def test_add_floats():
result = add(0.1, 0.2)
assert result == pytest.approx(0.3) # Handle float precision
def test_divide():
assert divide(10, 2) == 5.0
assert divide(7, 2) == 3.5
def test_divide_by_zero():
import pytest
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)
# Run tests:
# pytest # Run all tests
# pytest test_calculator.py # Run specific file
# pytest -v # Verbose output
# pytest -x # Stop on first failure
# pytest -k "test_add" # Run tests matching pattern
Fixtures
Fixtures provide a way to set up test data and resources. They are more powerful than setUp/tearDown methods because they are modular, composable, and support dependency injection.
import pytest
# Basic fixture
@pytest.fixture
def sample_users():
return [
{"name": "Alice", "age": 30, "role": "admin"},
{"name": "Bob", "age": 25, "role": "user"},
{"name": "Charlie", "age": 35, "role": "user"},
]
def test_user_count(sample_users):
assert len(sample_users) == 3
def test_admin_exists(sample_users):
admins = [u for u in sample_users if u["role"] == "admin"]
assert len(admins) == 1
# Fixture with setup and teardown
@pytest.fixture
def temp_file(tmp_path):
file_path = tmp_path / "test.txt"
file_path.write_text("test data")
yield file_path # test runs here
# Cleanup runs after test (automatic with tmp_path)
def test_read_file(temp_file):
assert temp_file.read_text() == "test data"
# Fixture scopes
@pytest.fixture(scope="module") # Created once per module
def db_connection():
conn = create_connection()
yield conn
conn.close()
@pytest.fixture(scope="session") # Created once per test session
def app_config():
return {"debug": True, "db": "sqlite:///:memory:"}
# Parametrized fixtures
@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
def database_type(request):
return request.param
def test_supported_database(database_type):
assert database_type in ["sqlite", "postgresql", "mysql"]
Parametrized Tests
import pytest
# Run the same test with different inputs
@pytest.mark.parametrize("input,expected", [
("hello", "HELLO"),
("world", "WORLD"),
("Python", "PYTHON"),
("", ""),
])
def test_uppercase(input, expected):
assert input.upper() == expected
@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
# Multiple parametrize decorators create combinations
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [10, 20])
def test_multiply(x, y):
assert x * y > 0 # Tests: (1,10), (1,20), (2,10), (2,20)
Mocking
from unittest.mock import Mock, patch, MagicMock
import pytest
# Simple mock
def test_mock_basic():
mock_db = Mock()
mock_db.get_user.return_value = {"id": 1, "name": "Alice"}
user = mock_db.get_user(1)
assert user["name"] == "Alice"
mock_db.get_user.assert_called_once_with(1)
# Patching external dependencies
# service.py
import requests
def get_weather(city):
response = requests.get(f"https://api.weather.com/{city}")
return response.json()
# test_service.py
@patch("service.requests.get")
def test_get_weather(mock_get):
mock_response = Mock()
mock_response.json.return_value = {"temp": 72, "condition": "sunny"}
mock_get.return_value = mock_response
result = get_weather("NYC")
assert result["temp"] == 72
mock_get.assert_called_once_with("https://api.weather.com/NYC")
# Context manager style
def test_get_weather_context():
with patch("service.requests.get") as mock_get:
mock_get.return_value.json.return_value = {"temp": 72}
result = get_weather("NYC")
assert result["temp"] == 72
# pytest-mock fixture (cleaner)
# pip install pytest-mock
def test_get_weather_mocker(mocker):
mock_get = mocker.patch("service.requests.get")
mock_get.return_value.json.return_value = {"temp": 72}
result = get_weather("NYC")
assert result["temp"] == 72
Key Takeaways
- Use pytest: Simpler syntax and better output than unittest
- Fixtures for setup: Modular, composable, and support dependency injection
- Parametrize tests: Test many inputs with one test function
- Mock external deps: Isolate your tests from networks, databases, and files