| name | pytest |
| description | [Applies to: **/*.py] This guide defines our team's definitive best practices for writing fast, reliable, and maintainable tests using pytest, ensuring consistency and high code quality across projects. |
| source | cursor_mdc |
pytest Best Practices
This document outlines our team's standard for writing pytest tests. Adhering to these guidelines ensures our test suite is fast, reliable, and easy to maintain, integrating seamlessly into our CI/CD pipelines.
1. Code Organization & Structure
1.1 Project Layout
Always use the src/ layout for your project. This prevents common import pitfalls and ensures tests run against the installed package, not local source.
✅ GOOD:
.
├── pyproject.toml
├── src/
│ └── mypackage/
│ ├── __init__.py
│ └── module.py
└── tests/
├── __init__.py
└── test_module.py
❌ BAD:
.
├── pyproject.toml
├── mypackage/ # Top-level package
│ ├── __init__.py
│ └── module.py
└── tests/
└── test_module.py
1.2 Test File Naming
Name test files test_*.py or *_test.py. We prefer test_*.py.
Place __init__.py files in tests/ and its subdirectories to treat them as Python packages, enabling structured imports and avoiding name collisions.
✅ GOOD:
from mypackage.module import my_function
def test_my_function_returns_expected_value():
assert my_function() == "expected"
1.3 Test Naming Conventions
Prefix test functions and methods with test_. For classes, prefix with Test. Use descriptive names that read like sentences.
✅ GOOD:
def test_user_can_be_created_with_valid_email():
pass
class TestUserService:
def test_get_user_by_id_returns_correct_user():
pass
❌ BAD:
def create_user():
pass
def test_create_user_and_email():
pass
class UserServiceTest:
pass
2. Common Patterns & Anti-patterns
2.1 Single Assert Per Test
Each test should verify one specific behavior. This improves readability and simplifies debugging.
✅ GOOD:
def test_user_creation_sets_email():
user = create_user("test@example.com")
assert user.email == "test@example.com"
def test_user_creation_sends_welcome_email():
user = create_user("test@example.com")
assert email_service.last_sent_email == "test@example.com"
❌ BAD:
def test_user_creation_and_email_sending():
user = create_user("test@example.com")
assert user.email == "test@example.com"
send_welcome_email(user)
assert email_service.last_sent_email == "test@example.com"
2.2 Fixtures for Setup/Teardown & Dependency Injection
Use fixtures to provide isolated, reusable resources and manage setup/teardown.
- Scope:
function (default): Run once per test function. Use for most tests.
class: Run once per test class.
module: Run once per test module. Use for expensive setups shared across module tests.
session: Run once per test session. Use for global, very expensive resources (e.g., database connection pool).
- Teardown: Use
yield in fixtures for automatic teardown.
- Modularity: Compose fixtures by injecting them into others.
- Avoid
autouse=True: Only use for truly global, non-interfering setups (e.g., patching a common library function for all tests).
✅ GOOD:
import pytest
@pytest.fixture(scope="function")
def db_session():
session = create_test_db_session()
yield session
session.close()
@pytest.fixture(scope="module")
def api_client(db_session):
client = TestClient(db_session)
yield client
❌ BAD:
class TestUser:
def setup_method(self):
self.user = User("test@example.com")
def test_user_email(self):
assert self.user.email == "test@example.com"
@pytest.fixture(autouse=True)
def global_patch():
pass
2.3 Parameterization
Use @pytest.mark.parametrize for data-driven tests.
✅ GOOD:
import pytest
@pytest.mark.parametrize("input, expected_output", [
(1, 2),
(2, 3),
(0, 1),
])
def test_increment_function(input, expected_output):
assert increment(input) == expected_output
2.4 Markers
Use @pytest.mark.skip for tests that are not yet ready or temporarily broken. Use @pytest.mark.xfail for known bugs that are expected to fail but should still be run.
✅ GOOD:
import pytest
@pytest.mark.skip(reason="Feature not implemented yet")
def test_new_feature_endpoint():
pass
@pytest.mark.xfail(reason="Known bug: #1234")
def test_buggy_legacy_function():
assert legacy_function() == "fixed_output"
3. Performance Considerations
3.1 Keep Tests Fast
Prioritize fast tests. Slow tests discourage local execution and lead to longer CI times.
- Mock external services: Use mocks for APIs, databases, and file systems.
- Appropriate fixture scope: Don't use
session scope for resources that need to be fresh per test.
- Small, focused tests: Each test should do minimal work.
4. Mocking Strategies
Use the mocker fixture (from pytest-mock) for robust mocking. It integrates unittest.mock functionality with pytest's fixture system.
✅ GOOD:
from mypackage.service import MyService
from mypackage.external_api import get_data
def test_my_service_fetches_data(mocker):
mock_get_data = mocker.patch("mypackage.service.get_data")
mock_get_data.return_value = {"key": "mocked_value"}
service = MyService()
result = service.fetch_and_process()
mock_get_data.assert_called_once()
assert result == "processed_mocked_value"
❌ BAD:
import pytest
from mypackage.service import MyService
from mypackage.external_api import get_data
def test_my_service_fetches_data_raw_monkeypatch(monkeypatch):
def mock_get_data():
return {"key": "mocked_value"}
monkeypatch.setattr("mypackage.service.get_data", mock_get_data)
service = MyService()
result = service.fetch_and_process()
assert result == "processed_mocked_value"
5. Coverage Patterns
Integrate pytest-cov to measure test coverage. Aim for high coverage, but understand that 100% coverage doesn't guarantee bug-free code. Focus on covering critical paths and edge cases.
✅ GOOD:
pytest --cov=mypackage --cov-report=term-missing --cov-report=html
6. Type Hints
Apply type hints to test code, especially for fixtures and complex test functions. This improves readability, maintainability, and enables static analysis.
✅ GOOD:
from typing import Generator
import pytest
class User:
def __init__(self, email: str):
self.email = email
@pytest.fixture
def new_user() -> Generator[User, None, None]:
user = User("test@example.com")
yield user
def test_user_email_property(new_user: User) -> None:
assert new_user.email == "test@example.com"