forked from ivangrynenko/cursorrules
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtesting-python.mdc
More file actions
238 lines (194 loc) · 7.48 KB
/
testing-python.mdc
File metadata and controls
238 lines (194 loc) · 7.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
---
description: Python testing standards using pytest, unittest, and mocking patterns
globs: test_*.py, *_test.py, tests/**/*.py
alwaysApply: false
tags:
- language:python
- category:testing
- framework:pytest
- framework:unittest
---
# Python Testing Standards
Enforces testing best practices for Python projects using pytest, unittest, and mocking patterns.
<rule>
name: python_testing_standards
description: Enforce testing best practices for Python including pytest, unittest, and mocking patterns
filters:
- type: file_extension
pattern: "\\.py$"
- type: file_path
pattern: ".*(test|spec|tests).*"
actions:
- type: enforce
conditions:
# Pattern 1: Missing test function naming
- pattern: "def\\s+test_|def\\s+Test"
message: "Test functions must start with 'test_' prefix. Test classes must start with 'Test' prefix."
# Pattern 2: Missing assertions
- pattern: "def\\s+test_[^(]*\\([^)]*\\)\\s*:\\s*(pass|return|#)"
message: "Test functions must contain assertions. Use assert statements or assertion methods."
# Pattern 3: Missing fixtures for setup
- pattern: "def\\s+test_[^(]*\\([^)]*\\)\\s*:\\s*[^a]*\\s*=\\s*(pd\\.read_|open\\(|sqlalchemy\\.|requests\\.)"
message: "Use pytest fixtures for test setup. Avoid creating resources directly in test functions."
# Pattern 4: Missing cleanup
- pattern: "def\\s+test_[^(]*\\([^)]*\\)\\s*:\\s*[^}]*\\.(create|insert|write|save)"
message: "Clean up resources after tests. Use fixtures with yield or teardown methods."
# Pattern 5: Hardcoded test data
- pattern: "def\\s+test_[^(]*\\([^)]*\\)\\s*:\\s*[^=]*=\\s*['\"][^'\"]+['\"]"
message: "Use fixtures or constants for test data. Avoid hardcoded values in tests."
# Pattern 6: Missing error testing
- pattern: "def\\s+test_[^(]*\\([^)]*\\)\\s*:\\s*[^}]*\\.(get|find|fetch|load)"
message: "Test error cases: missing data, invalid input, exceptions. Use pytest.raises() for exception testing."
- type: suggest
message: |
**Python Testing Best Practices:**
**Test Structure:**
- Use `pytest` as the testing framework
- Test functions must start with `test_` prefix
- Test classes must start with `Test` prefix
- Use descriptive test names: `test_should_return_user_when_user_exists`
- Follow AAA pattern: Arrange, Act, Assert
**Pytest Example:**
```python
import pytest
from unittest.mock import Mock, patch
def test_get_user_returns_user_when_exists():
# Arrange
user_id = '123'
expected_user = {'id': user_id, 'name': 'John'}
user_repository = Mock()
user_repository.find_by_id.return_value = expected_user
user_service = UserService(user_repository)
# Act
result = user_service.get_user(user_id)
# Assert
assert result == expected_user
user_repository.find_by_id.assert_called_once_with(user_id)
def test_get_user_raises_error_when_not_found():
# Arrange
user_id = '999'
user_repository = Mock()
user_repository.find_by_id.return_value = None
user_service = UserService(user_repository)
# Act & Assert
with pytest.raises(UserNotFoundError, match='User not found'):
user_service.get_user(user_id)
```
**Fixtures:**
- Use `@pytest.fixture` for test setup
- Use `yield` for teardown
- Share fixtures via `conftest.py`
- Use fixture scopes: function, class, module, session
**Example:**
```python
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture(scope='function')
def db_session():
engine = create_engine('sqlite:///:memory:')
Session = sessionmaker(bind=engine)
session = Session()
# Setup
Base.metadata.create_all(engine)
yield session
# Teardown
session.close()
Base.metadata.drop_all(engine)
def test_create_user(db_session):
user = User(name='John', email='john@example.com')
db_session.add(user)
db_session.commit()
assert user.id is not None
assert db_session.query(User).filter_by(email='john@example.com').first() == user
```
**Mocking:**
- Use `unittest.mock` or `pytest-mock` for mocking
- Mock external dependencies (APIs, databases, file system)
- Use `@patch` decorator for patching
- Use `Mock` and `MagicMock` for creating mock objects
**Example:**
```python
from unittest.mock import Mock, patch, MagicMock
@patch('requests.get')
def test_fetch_user_data(mock_get):
# Arrange
mock_response = Mock()
mock_response.json.return_value = {'id': '1', 'name': 'John'}
mock_response.status_code = 200
mock_get.return_value = mock_response
# Act
result = fetch_user_data('123')
# Assert
assert result == {'id': '1', 'name': 'John'}
mock_get.assert_called_once_with('https://api.example.com/users/123')
```
**Parametrized Tests:**
- Use `@pytest.mark.parametrize` for testing multiple inputs
- Test edge cases and boundary conditions
**Example:**
```python
@pytest.mark.parametrize('input,expected', [
('valid@email.com', True),
('invalid-email', False),
('', False),
('test@', False),
('@example.com', False),
])
def test_validate_email(input, expected):
assert validate_email(input) == expected
```
**Async Testing:**
- Use `pytest-asyncio` for async tests
- Use `@pytest.mark.asyncio` decorator
- Use `await` in async test functions
**Example:**
```python
import pytest
@pytest.mark.asyncio
async def test_async_fetch_user():
user = await fetch_user_async('123')
assert user is not None
assert user['id'] == '123'
```
**Data Pipeline Testing:**
- Test data transformations
- Validate data quality
- Test error handling and recovery
- Use fixtures for test data
**Example:**
```python
@pytest.fixture
def sample_data():
return pd.DataFrame({
'id': [1, 2, 3],
'name': ['John', 'Jane', 'Bob'],
'email': ['john@example.com', 'jane@example.com', 'bob@example.com']
})
def test_transform_data(sample_data):
result = transform_data(sample_data)
assert 'email_domain' in result.columns
assert result['email_domain'].iloc[0] == 'example.com'
assert len(result) == len(sample_data)
```
**Test Coverage:**
- Use `pytest-cov` for coverage reporting
- Aim for high coverage (80%+)
- Focus on critical paths
- Test edge cases and error conditions
**Running Tests:**
```bash
# Run all tests
pytest
# Run with coverage
pytest --cov=src --cov-report=html
# Run specific test
pytest tests/test_user_service.py::test_get_user
# Run with verbose output
pytest -v
```
metadata:
priority: high
version: 1.0.0
lastUpdated: 2025-12-05
</rule>