pytest — Cursor rule: a team standard for pytest tests
A Cursor rule that pins down how pytest tests get written: layout, fixtures, parametrization, mocks. Useful once more than one person touches the suite.
.cursor/rules/pytest.mdcWhat pytest does
Drop .cursor/rules/pytest.mdc into a project and Cursor feeds it to the agent whenever Python files are in play. The document covers layout first: a src/ tree, test files named test_*.py, __init__.py inside tests/, function names starting with test_ and classes with Test.
Then the patterns. One assertion per test. Fixtures carry setup and teardown through yield, with function, class, module and session scopes spelled out; autouse=True is discouraged outside genuinely global patches. Data-driven cases go through @pytest.mark.parametrize, unfinished or broken tests through skip and xfail.
Mocking gets its own section: the mocker fixture from pytest-mock is preferred over raw monkeypatch when the mock needs assertion helpers. Coverage means adding pytest-cov. The globs field is **/*.py, so the rule applies to every Python file — if your tests sit next to the code instead of under tests/, the import advice about running against the installed package no longer matches your setup.
What it helps with
- settle on names for test files and functions
- move setup and cleanup into fixtures
- run one scenario across several input sets
- mark a known-broken test as expected to fail
- swap a live API call for a mock inside a test
Example prompts for the agent
- Write tests for
mypackage/service.pyfollowing the pytest rule - Rewrite this test with
@pytest.mark.parametrize - Replace monkeypatch with the
mockerfixture intests/test_service.py
How to install pytest
Manually: put the files in place
.cursor/rules/pytest.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/pytest.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/pytest.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/pytest.mdManually: put the files in place
.clinerules/pytest.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about pytest
How do I install the pytest rule in Cursor?
Place the file at .cursor/rules/pytest.mdc in your project root. Cursor picks it up as a project rule.
What does the pytest rule do?
It lays out a pytest standard: src/ layout, file and function naming, fixtures, parametrization, markers, mocking and coverage. The agent follows it while writing or editing tests.
Which files does it apply to?
The globs field is set to **/*.py, so the rule covers every Python file in the project.
Source text
pytest.mdc · CC0-1.0
---
description: 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.
globs: **/*.py
---
# 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:**
```python
# tests/test_my_feature.py
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:**
```python
def test_user_can_be_created_with_valid_email():
# ...
pass
class TestUserService:
def test_get_user_by_id_returns_correct_user():
# ...
pass
```
**❌ BAD:**
```python
def create_user(): # Not discovered by pytest
pass
def test_create_user_and_email(): # Tests multiple things
pass
class UserServiceTest: # Incorrect class prefix
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:**
```python
def test_user_creation_sets_email():
user = create_user("[email protected]")
assert user.email == "[email protected]"
def test_user_creation_sends_welcome_email():
user = create_user("[email protected]")
assert email_service.last_sent_email == "[email protected]"
```
**❌ BAD:**
```python
def test_user_creation_and_email_sending():
user = create_user("[email protected]")
assert user.email == "[email protected]" # First assert
send_welcome_email(user)
assert email_service.last_sent_email == "[email protected]" # Second assert
```
### 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:**
```python
# conftest.py
import pytest
@pytest.fixture(scope="function")
def db_session():
# Setup: Create a fresh database session
session = create_test_db_session()
yield session # Test runs here
# Teardown: Close and clean up the session
session.close()
@pytest.fixture(scope="module")
def api_client(db_session): # Fixtures can use other fixtures
# Setup: Create an API client with a specific DB session
client = TestClient(db_session)
yield client
# Teardown: Cleanup client resources
```
**❌ BAD:**
```python
# test_bad.py
class TestUser:
def setup_method(self): # unittest-style setup, less flexible
self.user = User("[email protected]")
def test_user_email(self):
assert self.user.email == "[email protected]"
@pytest.fixture(autouse=True) # Avoid unless absolutely necessary
def global_patch():
# This fixture runs for *every* test, potentially causing unexpected side effects.
pass
```
### 2.3 Parameterization
Use `@pytest.mark.parametrize` for data-driven tests.
**✅ GOOD:**
```python
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:**
```python
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:**
```python
# test_service.py
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:**
```python
# test_service.py (using raw monkeypatch for complex mocks)
import pytest
from mypackage.service import MyService
from mypackage.external_api import get_data
def test_my_service_fetches_data_raw_monkeypatch(monkeypatch):
# monkeypatch is fine for simple cases, but mocker is better for complex mocks
# and provides more assert methods.
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:**
```bash
# Run tests with coverage and generate report
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:**
```python
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("[email protected]")
yield user
# Cleanup if necessary
def test_user_email_property(new_user: User) -> None:
assert new_user.email == "[email protected]"
```
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc