Writing tests shouldn't feel like a chore. With the right pytest patterns, tests become fast to write and easy to maintain.
Why pytest Over unittest
# unittest — verbose
import unittest
class TestMath(unittest.TestCase):
def test_add(self):
self.assertEqual(1 + 1, 2)
# pytest — clean
def test_add():
assert 1 + 1 == 2
Less boilerplate, better error messages, powerful fixtures.
Fixtures: Setup Done Right
import pytest
@pytest.fixture
def sample_user():
return {"name": "Alice", "email": "alice@example.com", "role": "admin"}
@pytest.fixture
def db_connection():
conn = create_connection("test.db")
yield conn # test runs here
conn.close() # teardown after test
def test_user_is_admin(sample_user):
assert sample_user["role"] == "admin"
def test_insert_user(db_connection, sample_user):
db_connection.insert("users", sample_user)
result = db_connection.find("users", {"name": "Alice"})
assert result is not None
Parametrize: One Test, Many Cases
@pytest.mark.parametrize("input_val,expected", [
("hello", 5),
("", 0),
("world!", 6),
(" spaces ", 10),
])
def test_string_length(input_val, expected):
assert len(input_val) == expected
@pytest.mark.parametrize("a,b,result", [
(1, 2, 3),
(-1, 1, 0),
(0, 0, 0),
(100, -50, 50),
])
def test_addition(a, b, result):
assert a + b == result
Mocking External Services
from unittest.mock import patch, MagicMock
def get_weather(city):
import requests
response = requests.get(f"https://api.weather.com/{city}")
return response.json()["temperature"]
def test_get_weather():
mock_response = MagicMock()
mock_response.json.return_value = {"temperature": 72}
with patch("requests.get", return_value=mock_response) as mock_get:
temp = get_weather("NYC")
assert temp == 72
mock_get.assert_called_once_with("https://api.weather.com/NYC")
Testing Exceptions
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
def test_divide_by_zero():
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)
Temporary Files and Directories
def test_file_processing(tmp_path):
# tmp_path is a built-in fixture
test_file = tmp_path / "data.txt"
test_file.write_text("line1\nline2\nline3")
result = count_lines(str(test_file))
assert result == 3
conftest.py: Shared Fixtures
# conftest.py
import pytest
@pytest.fixture(scope="session")
def api_client():
client = TestClient(app)
yield client
@pytest.fixture(autouse=True)
def reset_db(db_connection):
yield
db_connection.rollback()
Useful CLI Options
pytest -v # verbose output
pytest -x # stop on first failure
pytest -k "test_user" # run tests matching pattern
pytest --tb=short # shorter tracebacks
pytest -n auto # parallel execution (pytest-xdist)
pytest --cov=myapp # coverage report
Testing Async Code
import pytest
@pytest.mark.asyncio
async def test_async_fetch():
result = await fetch_data("https://api.example.com")
assert result["status"] == "ok"
Key Patterns
- One assertion per test when possible
- Use fixtures for setup, not helper functions in tests
- Parametrize instead of copy-pasting similar tests
- Mock at the boundary (API calls, database, file system)
- Use
conftest.pyfor shared fixtures - Run with
--covto track coverage
Good tests are fast, isolated, and readable. pytest makes all three easy.
🚀 Level up your AI workflow! Check out my AI Developer Mega Prompt Pack — 80 battle-tested prompts for developers. $9.99
Top comments (0)