pytest is the testing framework that makes Python tests a joy to write — if you know its features. Most developers use pytest as a basic test runner and miss fixtures with scope, parametrize for data-driven tests, conftest.py for shared setup, and the patching patterns that make external dependencies testable. This guide focuses on pytest testing, with the production trade-offs senior engineers need to make sound decisions.
⚡ TL;DR: Fixtures for reusable setup/teardown with scope control. @pytest.mark.parametrize for data-driven tests. conftest.py for shared fixtures across test files. unittest.mock.patch for external dependencies. pytest-cov for coverage. pytest.raises for exception testing.
Fixtures — reusable test setup — pytest testing
import pytest
# Function scope (default): new fixture per test
@pytest.fixture
def user_data():
return {'name':'Alice','email':'alice@example.com','age':30}
# Module scope: created once per module
@pytest.fixture(scope='module')
def db_connection():
conn = create_test_db()
yield conn # Provide to test
conn.close() # Cleanup after ALL tests in module
# Session scope: created once for entire test session
@pytest.fixture(scope='session')
def app_client():
from myapp import create_app
app = create_app({'TESTING':True,'DB':'sqlite:///:memory:'})
with app.test_client() as client:
yield client
# Using fixtures:
def test_create_user(db_connection, user_data):
result = db_connection.create_user(**user_data)
assert result.id is not None
assert result.email == user_data['email']parametrize — data-driven tests
@pytest.mark.parametrize('email,valid', [
('alice@example.com', True),
('bob@test.co.uk', True),
('notanemail', False),
('@nodomain.com', False),
('', False),
('a' * 250 + '@example.com', False), # Too long
])
def test_email_validation(email, valid):
assert validate_email(email) == valid
# Multiple parameters:
@pytest.mark.parametrize('a,b,expected', [
(1, 2, 3), (0, 0, 0), (-1, 1, 0), (100, -1, 99)
])
def test_add(a, b, expected):
assert add(a, b) == expected
# Generate 6 test cases from one parametrize — efficient!Mocking external dependencies
from unittest.mock import patch, Mock, AsyncMock
def test_send_email_on_user_creation():
with patch('myapp.email_service.send') as mock_send:
mock_send.return_value = {'status':'sent','id':'msg-123'}
result = create_user({'name':'Alice','email':'alice@example.com'})
assert result.id is not None
mock_send.assert_called_once_with(
to='alice@example.com',
template='welcome'
)
# Async mocking:
async def test_fetch_user():
with patch('myapp.http_client.get', new_callable=AsyncMock) as mock_get:
mock_get.return_value = Mock(json=AsyncMock(return_value={'id':1,'name':'Alice'}))
user = await fetch_user(1)
assert user['name'] == 'Alice'conftest.py and advanced patterns
# conftest.py — shared across test files, no import needed
import pytest
from myapp import create_app
from myapp.database import db
@pytest.fixture(scope='session')
def app():
app = create_app({'TESTING':True,'SQLALCHEMY_DATABASE_URI':'sqlite:///:memory:'})
with app.app_context():
db.create_all()
yield app
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture(autouse=True) # Applied to ALL tests automatically
def reset_db(app):
yield
with app.app_context():
db.session.rollback() # Reset after each test
# pytest.ini — configure coverage:
[pytest]
addopts = --cov=myapp --cov-report=term-missing --cov-fail-under=80- ✅ Function scope fixtures for test isolation, session scope for expensive setup
- ✅ parametrize for data-driven tests — test many cases in one function
- ✅ conftest.py for shared fixtures — no import needed
- ✅ autouse=True for fixtures that apply to all tests (DB reset)
- ✅ AsyncMock for async functions
- ❌ Never use real external services in unit tests — always mock
- ❌ Never share mutable state between tests — causes order-dependent failures
External reference: pytest documentation.
Recommended Reading
→ Designing Data-Intensive Applications — The bible of distributed systems and production engineering at scale.
→ The Pragmatic Programmer — Timeless engineering wisdom every senior developer needs.
Affiliate links. We earn a small commission at no extra cost to you.
Related on CheatCoders: unit testing best practices that catch production bugs, Python context managers, and Python type hints.
Free Weekly Newsletter
🚀 Join 2,000+ Senior Developers
Get expert-level JavaScript, Python, AWS, system design and AI secrets every week. Zero fluff, pure signal.
Related: Python Dataclasses: 10 Advanced Features That Make __init__ Obsolete · Lambda Test Events as Contracts: Schema-Locked Fixtures Across Teams · Python Decorators Explained: From Simple Wrappers to Production Patterns
Related: Python asyncio vs Threading vs Multiprocessing: When to Use Each · JavaScript Promises: The Complete Guide From Basics to Advanced Patterns · Python Dictionary Methods and Tricks: The Complete Guide
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Agentic Git Workflows: Atomic Commits From Noisy LLM Diffs
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
Newly added
- Amazon Bedrock Model Evaluation: Score Coding-Agent Outputs Before You Promote a Prompt
- AWS App Runner: Host Coding-Agent HTTP APIs Without Babysitting Containers
- Amazon S3 Object Lock: Immutable Artifact Buckets Coding Agents Cannot Overwrite
- AWS Security Hub: Aggregate Coding-Agent Security Findings Across Accounts
- Amazon Detective: Investigate Compromised Coding-Agent Credentials After GuardDuty
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
