- name
- pytest-patterns
- description
- Python testing skill using pytest, covering fixtures, parametrize, markers, conftest, plugins, mocking, and advanced testing patterns.
- license
- MIT
- metadata
- {"author":"thetestingacademy","version":"1.0.0","source":"https://qaskills.sh/skills/thetestingacademy/pytest-patterns"}
# Pytest Patterns Skill
You are an expert Python developer specializing in testing with pytest. When the user asks you to write, review, or debug pytest tests, follow these detailed instructions.
## Core Principles
1. **Convention over configuration** -- pytest discovers tests automatically by naming conventions.
2. **Fixtures for setup** -- Use fixtures instead of setUp/tearDown methods.
3. **Parametrize for coverage** -- Use `@pytest.mark.parametrize` for data-driven tests.
4. **Descriptive test names** -- Function names should describe the expected behavior.
5. **Minimal test scope** -- Each test verifies one behavior.
## Project Structure
```
project/
src/
myapp/
__init__.py
services/
user_service.py
order_service.py
models/
user.py
utils/
validators.py
tests/
__init__.py
conftest.py
unit/
__init__.py
test_user_service.py
test_validators.py
integration/
__init__.py
conftest.py
test_user_api.py
fixtures/
user_fixtures.py
pyproject.toml
pytest.ini
```
## Configuration
```ini
# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short --strict-markers
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks integration tests
smoke: marks smoke tests
unit: marks unit tests
```
```toml
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short --strict-markers --cov=src --cov-report=term-missing"
markers = [
"slow: marks tests as slow",
"integration: marks integration tests",
"smoke: marks smoke tests",
]
[tool.coverage.run]
source = ["src"]
omit = ["tests/*", "*/__init__.py"]
[tool.coverage.report]
fail_under = 80
show_missing = true
```
## Fixtures
### Basic Fixtures
```python
# conftest.py
import pytest
from myapp.services.user_service import UserService
from myapp.models.user import User
@pytest.fixture
def sample_user():
"""Create a sample user for testing."""
return User(
id="user-123",
email="test@example.com",
name="Test User",
role="user",
)
@pytest.fixture
def admin_user():
"""Create an admin user for testing."""
return User(
id="admin-123",
email="admin@example.com",
name="Admin User",
role="admin",
)
@pytest.fixture
def user_service(mock_user_repo, mock_email_service):
"""Create UserService with mocked dependencies."""
return UserService(
user_repo=mock_user_repo,
email_service=mock_email_service,
)
```
### Fixture Scopes
```python
@pytest.fixture(scope="session")
def database_connection():
"""Create a database connection once for the entire test session."""
conn = create_connection("test_db")
yield conn
conn.close()
@pytest.fixture(scope="module")
def test_data(database_connection):
"""Seed test data once per module."""
seed_test_data(database_connection)
yield
cleanup_test_data(database_connection)
@pytest.fixture(scope="function") # default scope
def fresh_user():
"""Create a fresh user for each test function."""
return create_user(email=f"test-{uuid4()}@example.com")
@pytest.fixture(scope="class")
def shared_resource():
"""Share a resource across all methods in a test class."""
resource = create_expensive_resource()
yield resource
resource.cleanup()
```
### Fixture Factories
```python
@pytest.fixture
def make_user():
"""Factory fixture that creates users with custom attributes."""
created_users = []
def _make_user(
email: str = None,
name: str = "Test User",
role: str = "user",
) -> User:
user = User(
id=str(uuid4()),
email=email or f"test-{uuid4()}@example.com",
name=name,
role=role,
)
created_users.append(user)
return user
yield _make_user
# Cleanup
for user in created_users:
try:
delete_user(user.id)
except Exception:
pass
# Usage in tests
def test_admin_permissions(make_user):
admin = make_user(role="admin")
viewer = make_user(role="viewer")
assert admin.can_delete_users()
assert not viewer.can_delete_users()
```
### Yield Fixtures (Setup/Teardown)
```python
@pytest.fixture
def temp_file(tmp_path):
"""Create a temporary file and clean up after test."""
file_path = tmp_path / "test_data.json"
file_path.write_text('{"key": "value"}')
yield file_path
# Teardown happens automatically (tmp_path handles cleanup)
@pytest.fixture
def mock_server():
"""Start a mock HTTP server for testing."""
server = MockServer(port=8089)
server.start()
yield server
server.stop()
@pytest.fixture
def db_transaction(database_connection):
"""Wrap each test in a database transaction that rolls back."""
transaction = database_connection.begin()
yield database_connection
transaction.rollback()
```
## Parametrize
### Basic Parametrize
```python
@pytest.mark.parametrize("email,expected", [
("user@example.com", True),
("first.last@domain.co.uk", True),
("user+tag@example.com", True),
("", False),
("not-an-email", False),
("@missing-local.com", False),
("missing-at.com", False),
])
def test_is_valid_email(email, expected):
assert is_valid_email(email) == expected
```
### Multiple Parameters
```python
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
def test_add(a, b, expected):
assert add(a, b) == expected
```
### Parametrize with IDs
```python
@pytest.mark.parametrize(
"password,is_valid",
[
pytest.param("SecurePass1!", True, id="strong-password"),
pytest.param("short", False, id="too-short"),
pytest.param("nouppercase1!", False, id="no-uppercase"),
pytest.param("NOLOWERCASE1!", False, id="no-lowercase"),
pytest.param("NoSpecialChar1", False, id="no-special-char"),
],
)
def test_password_validation(password, is_valid):
assert validate_password(password) == is_valid
```
### Combining Parametrize Decorators
```python
@pytest.mark.parametrize("method", ["GET", "POST", "PUT", "DELETE"])
@pytest.mark.parametrize("auth", [True, False])
def test_api_endpoint_auth(method, auth, api_client):
"""Test each HTTP method with and without authentication."""
response = api_client.request(method, "/protected", authenticated=auth)
if auth:
assert response.status_code != 401
else:
assert response.status_code == 401
```
## Markers
```python
# Define custom markers in conftest.py or pytest.ini
@pytest.mark.slow
def test_large_file_processing():
"""This test takes a long time to run."""
result = process_large_file("100mb_dataset.csv")
assert result.row_count == 1_000_000
@pytest.mark.integration
def test_database_connection():
"""Requires a running database."""
conn = connect_to_db()
assert conn.is_connected()
@pytest.mark.smoke
def test_health_check(api_client):
"""Quick check that the service is running."""
response = api_client.get("/health")
assert response.status_code == 200
@pytest.mark.skip(reason="Feature not yet implemented")
def test_future_feature():
pass
@pytest.mark.skipif(
sys.platform == "win32",
reason="Not supported on Windows"
)
def test_unix_specific():
pass
@pytest.mark.xfail(reason="Known bug #1234")
def test_known_issue():
assert buggy_function() == expected_value
```
## Mocking with pytest-mock
```python
# Install: pip install pytest-mock
def test_create_user_sends_email(mocker, user_service):
"""Verify that creating a user sends a welcome email."""
mock_send = mocker.patch.object(
user_service.email_service,
"send_welcome_email",
)
mocker.patch.object(
user_service.user_repo,
"find_by_email",
return_value=None,
)
mocker.patch.object(
user_service.user_repo,
"create",
return_value=User(id="1", email="new@example.com", name="New"),
)
user_service.create_user("new@example.com", "New")
mock_send.assert_called_once_with("new@example.com", "New")
def test_api_call_with_retry(mocker):
"""Test that the function retries on failure."""
mock_get = mocker.patch("requests.get")
mock_get.side_effect = [
ConnectionError("Failed"),
ConnectionError("Failed"),
mocker.Mock(status_code=200, json=lambda: {"data": "success"}),
]
result = fetch_with_retry("/api/data", max_retries=3)
assert result == {"data": "success"}
assert mock_get.call_count == 3
def test_datetime_mocking(mocker):
"""Mock the current time for deterministic testing."""
fixed_now = datetime(2024, 6, 15, 12, 0, 0)
mocker.patch("myapp.services.datetime")
mocker.patch("myapp.services.datetime.now", return_value=fixed_now)
result = get_greeting()
assert result == "Good afternoon"
```
## Conftest Patterns
```python
# tests/conftest.py -- shared across all tests
import pytest
from unittest.mock import MagicMock
@pytest.fixture
def mock_user_repo():
"""Create a mock UserRepository."""
repo = MagicMock()
repo.find_by_email.return_value = None
repo.find_by_id.return_value = None
repo.create.side_effect = lambda data: {**data, "id": "generated-id"}
return repo
@pytest.fixture
def mock_email_service():
"""Create a mock EmailService."""
return MagicMock()
@pytest.fixture(autouse=True)
def reset_environment():
"""Automatically reset environment state before each test."""
import os
original_env = os.environ.copy()
yield
os.environ.clear()
os.environ.update(original_env)
```
```python
# tests/integration/conftest.py -- shared only within integration tests
import pytest
import httpx
@pytest.fixture(scope="session")
def api_base_url():
return os.getenv("API_BASE_URL", "http://localhost:3000")
@pytest.fixture
def api_client(api_base_url):
"""Create an HTTP client for API testing."""
View on GitHub