Pytest Cursor Rules: Python Testing Best Practices
Cursor rules for Pytest covering fixtures, parametrized tests, mocking with monkeypatch/pytest-mock, exception testing, conftest.py patterns, temporary files, and plugin configuration for fast, maintainable Python tests.

Overview
Pytest is the standard Python testing framework, preferred over unittest for its concise syntax, powerful fixture system, rich parametrization, and extensive plugin ecosystem. These cursor rules enforce proper test discovery, fixture scoping and teardown, parametrized test patterns, mocking with pytest-mock and monkeypatch, exception testing with pytest.raises, conftest.py hierarchy, and configuration best practices so AI assistants generate fast, maintainable Pytest suites.
Note:
Enforces test_ prefix naming, fixture scoping with yield teardown, @pytest.mark.parametrize for test combinations, mocker.patch at import site, pytest.raises with match patterns, conftest.py for shared fixtures, tmp_path for temporary files, and pytest.ini for registered markers and test paths.
Rules Configuration
---
description: Enforces Pytest best practices including fixture scoping, parametrized tests, mocking patterns, exception testing, conftest.py hierarchy, temporary file management, and plugin configuration.
globs: **/test_*.py,**/*_test.py,tests/**/*.py,conftest.py,pytest.ini,pyproject.toml
---
# Pytest Best Practices
You are an expert in Pytest, test-driven development, and Python testing patterns.
You understand fixtures, parametrization, mocking, test isolation, and pytest plugin configuration.
### Test Discovery & Naming
- Name test files: `test_*.py` or `*_test.py` in a `tests/` directory or alongside source files
- Name test functions: `test_*` with descriptive names — `test_raises_error_for_negative_amount()`
- Name test classes: `Test*` (e.g., `TestUserService`) — no `__init__` method
- NEVER add `__init__.py` to test directories — it breaks namespace package discovery
- Group tests logically: `tests/unit/`, `tests/integration/`, `tests/e2e/`
- Use `pytest.mark` for categorization: `@pytest.mark.unit`, `@pytest.mark.slow`
- Run subsets with markers: `pytest -m "not slow"`, `pytest -m unit`
### Fixtures
- Define fixtures with `@pytest.fixture` decorator
- Use `scope=` parameter: `'function'` (default), `'class'`, `'module'`, `'package'`, `'session'`
- Use `yield` for teardown — code after yield runs on fixture cleanup
- Use `autouse=True` sparingly — only for truly global setup like logging config
- Share fixtures across test files via `conftest.py` — fixtures are discovered automatically
- Factory fixtures return a function: `def make_user(): return lambda **kwargs: User(**kwargs)`
- Use `request.addfinalizer(fn)` as alternative to yield for complex teardown
- Use `request.param` inside parametrized fixtures
- NEVER import fixtures directly from conftest — they're injected by name
- Prefer fixture injection over module-level constants for test data
### Parametrized Tests
- Use `@pytest.mark.parametrize('input,expected', [...])` for test combinations
- Include edge cases in parametrize lists: empty strings, None, zero, negative numbers
- Use `ids=` parameter for human-readable test names: `ids=['empty_list', 'single_item', 'multiple_items']`
- Combine with fixtures: `@pytest.mark.parametrize('threshold', [0, 10, 100])` with `def test_filter(fixture_data, threshold)`
- Use `pytest_generate_tests` metafunc hook for dynamic parametrization at collection time
- Multiple parametrize decorators stack as a Cartesian product
- NEVER put assumptions about test order in parametrized tests
### Mocking (pytest-mock & monkeypatch)
- Use `mocker` fixture (pytest-mock plugin): `mocker.patch('module.function', return_value=42)`
- Patch at the IMPORT site, not definition: `mocker.patch('myapp.services.send_email')` not `'email.send'`
- Use `monkeypatch.setattr(obj, 'attr', mock_value)` for simple attribute replacement
- Use `monkeypatch.setenv('KEY', 'value')` for environment variables
- Use `monkeypatch.delenv('KEY', raising=False)` to remove environment variables
- Always restore with `mocker.stopall()` — pytest-mock auto-restores but explicit is safer
- Use `mocker.spy(obj, 'method')` to observe real calls without replacing behavior
- Assert on mocks: `mocked_fn.assert_called_once_with(expected_arg)`
- Use `mocker.patch.object()` when patching instance methods
- NEVER mock the module under test — only its dependencies
### Exception Testing
- Use `pytest.raises(ExceptionType) as exc_info:` context manager
- Use `pytest.raises(ValueError, match='expected pattern')` to assert on error message
- Use `pytest.warns(DeprecationWarning)` for warning assertions
- Assert on exception attributes: `assert exc_info.value.code == 400`
- Test both that exceptions ARE raised and that they contain the right information
- NEVER use bare `except:` in test code — catch specific exceptions
- For async exceptions, use `pytest.raises` with async context manager (Pytest 7.2+)
### Temporary Files & Directories
- Use `tmp_path` (pathlib.Path) fixture over `tmpdir` (py.path.local) — it's the modern API
- Create files: `(tmp_path / 'test.csv').write_text('col1,col2\n1,2')`
- Check file contents: `assert (tmp_path / 'output.json').read_text() == expected_json`
- NEVER hardcode `/tmp/` paths — use fixtures for test isolation
- Use `tmp_path_factory` for session/module-scoped temp directories
- Clean up is automatic — tmp_path directories are deleted after test completion unless `--basetemp` is set
### Configuration (pytest.ini / pyproject.toml)
- Register custom markers in pytest.ini: `markers = slow: marks tests as slow (deselect with '-m "not slow"')`
- Set `testpaths = tests` to specify test discovery root
- Configure `addopts = -v --tb=short --strict-markers` for default CLI options
- Use `filterwarnings = error` to turn warnings into errors
- Set `python_files = test_*.py *_test.py` for test file patterns
- Set `python_classes = Test*` for test class patterns
- Set `python_functions = test_*` for test function patterns
- Prefer pyproject.toml `[tool.pytest.ini_options]` over pytest.ini for modern projects
### Plugin Usage
- pytest-cov: generates coverage reports, configure with `--cov=src --cov-report=html`
- pytest-xdist: parallel test execution with `-n auto` (use with process-safe tests only)
- pytest-asyncio: async test support with `@pytest.mark.asyncio` and `asyncio_mode = auto`
- pytest-timeout: set per-test timeout with `@pytest.mark.timeout(30)`
- pytest-sugar: improved output formatting in development
- pytest-randomly: randomizes test order to detect ordering dependencies
Installation
Create pytest.mdc in your project's .cursor/rules/ directory and paste the configuration above. Cursor and Windsurf both read .cursor/rules/ — Copilot users place it in .github/copilot-instructions.md instead.
pip install pytest pytest-mock pytest-cov pytest-xdist
# pyproject.toml configuration
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short --strict-markers"
markers = [
"slow: marks tests as slow",
"integration: marks tests as integration tests",
]
# Run tests
pytest
pytest -m "not slow"
pytest -n auto --cov=src
Examples
# tests/conftest.py — Shared fixtures with scoping and teardown
import pytest
from myapp.database import create_connection, migrate
from myapp.models import User
@pytest.fixture(scope="session")
def db():
connection = create_connection(":memory:")
migrate(connection)
yield connection
connection.close()
@pytest.fixture
def user_factory(db):
def _create(**kwargs):
defaults = {"name": "Test User", "email": "[email protected]"}
return User.create(db, **{**defaults, **kwargs})
return _create
@pytest.fixture
def admin_user(user_factory):
return user_factory(name="Admin", role="admin")
# tests/test_user_service.py — Parametrized tests with mocking and exceptions
import pytest
from myapp.services import UserService, ValidationError
class TestUserService:
def test_create_user_success(self, mocker, user_factory):
mocker.patch("myapp.services.send_welcome_email")
mock_db = mocker.Mock()
user = UserService.create(mock_db, name="Alice", email="[email protected]")
mock_db.save.assert_called_once()
assert user.name == "Alice"
@pytest.mark.parametrize("name,email,expected_error", [
("", "[email protected]", "Name is required"),
("Alice", "", "Email is required"),
("Alice", "invalid", "Invalid email format"),
], ids=["empty_name", "empty_email", "bad_email"])
def test_create_user_validation(self, name, email, expected_error, mocker):
mock_db = mocker.Mock()
with pytest.raises(ValidationError, match=expected_error):
UserService.create(mock_db, name=name, email=email)
def test_create_user_rolls_back_on_email_failure(self, mocker):
mock_db = mocker.Mock()
mocker.patch("myapp.services.send_welcome_email", side_effect=Exception("Email service down"))
with pytest.raises(Exception, match="Email service down"):
UserService.create(mock_db, name="Alice", email="[email protected]")
mock_db.rollback.assert_called_once()
Related Resources
Related Articles
Vitest Cursor Rules: Next-Gen Unit Testing
Cursor rules for Vitest covering test structure, mocking with vi.fn/vi.mock, snapshot testing, code coverage, browser mode, workspace config, and performance optimization for fast unit tests.
Programming Languages Supported by Cursor Rules
Explore programming languages supported by Cursor Rules with language-specific guidelines, best practices, and examples for effective AI-assisted coding.
Elixir Cursor Rules: AI-Powered Development Best Practices
Cursor rules for Elixir development enforcing official style guides, modern Elixir 1.12+ features, and clean code principles with AI assistance for production-ready code.