metacoding 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -42
- package/LICENSE +1 -1
- package/lib/commands/init.d.ts.map +1 -1
- package/lib/commands/init.js +1 -1
- package/lib/commands/init.js.map +1 -1
- package/lib/services/template-manager.d.ts +4 -1
- package/lib/services/template-manager.d.ts.map +1 -1
- package/lib/services/template-manager.js +129 -10
- package/lib/services/template-manager.js.map +1 -1
- package/lib/services/vscode.js +1 -1
- package/lib/services/vscode.js.map +1 -1
- package/package.json +12 -4
- package/templates/general/code-review.instructions.md +265 -0
- package/templates/general/{files/copilot-instructions.md.template → copilot-instructions.md} +97 -140
- package/templates/{python/files → general}/docs-update.instructions.md +45 -32
- package/templates/general/release.instructions.md +242 -0
- package/templates/general/test-runner.instructions.md +188 -0
- package/templates/node/nodejs.coding.instructions.md +249 -0
- package/templates/node/nodejs.docs.instructions.md +234 -0
- package/templates/node/nodejs.testing.instructions.md +373 -0
- package/templates/python/python.coding.instructions.md +339 -0
- package/templates/python/python.docs.instructions.md +1147 -0
- package/templates/python/python.testing.instructions.md +1074 -0
- package/templates/react/react.coding.instructions.md +695 -0
- package/templates/react/react.docs.instructions.md +427 -0
- package/templates/react/react.testing.instructions.md +193 -0
- package/templates/react/test-runner.instructions.md +135 -0
- package/templates/typescript/template.json +16 -0
- package/templates/typescript/typescript.coding.instructions.md +368 -0
- package/templates/typescript/typescript.docs.instructions.md +734 -0
- package/templates/typescript/typescript.testing.instructions.md +740 -0
- package/templates/general/files/code-review.instructions.md +0 -111
- package/templates/general/files/docs-update.instructions.md +0 -203
- package/templates/general/files/release.instructions.md +0 -72
- package/templates/general/files/test-runner.instructions.md +0 -107
- package/templates/node/files/code-review.instructions.md +0 -222
- package/templates/node/files/copilot-instructions.md.template +0 -391
- package/templates/node/files/docs-update.instructions.md +0 -203
- package/templates/node/files/release.instructions.md +0 -72
- package/templates/node/files/test-runner.instructions.md +0 -108
- package/templates/python/files/code-review.instructions.md +0 -215
- package/templates/python/files/copilot-instructions.md.template +0 -418
- package/templates/python/files/release.instructions.md +0 -72
- package/templates/python/files/test-runner.instructions.md +0 -108
- package/templates/react/files/code-review.instructions.md +0 -160
- package/templates/react/files/copilot-instructions.md.template +0 -472
- package/templates/react/files/docs-update.instructions.md +0 -203
- package/templates/react/files/release.instructions.md +0 -72
- package/templates/react/files/test-runner.instructions.md +0 -108
|
@@ -0,0 +1,1074 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 'Python-specific testing patterns and frameworks (pytest, Django, FastAPI)'
|
|
3
|
+
applyTo: 'test/**/*.py,tests/**/*.py,**/test_*.py'
|
|
4
|
+
language: 'python'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Python Testing Standards
|
|
8
|
+
|
|
9
|
+
## Test Case Naming Conventions
|
|
10
|
+
|
|
11
|
+
### Test Case ID Format: `[AREA]-[TYPE]-[NUMBER]`
|
|
12
|
+
|
|
13
|
+
**Python/Django/FastAPI Area Prefixes:**
|
|
14
|
+
|
|
15
|
+
- `VIEW` - Django views/FastAPI endpoints tests
|
|
16
|
+
- `MODEL` - Django models/SQLAlchemy tests
|
|
17
|
+
- `FORM` - Django forms/Pydantic validators tests
|
|
18
|
+
- `API` - REST API endpoint tests
|
|
19
|
+
- `SRV` - Service layer tests
|
|
20
|
+
- `DB` - Database/ORM tests
|
|
21
|
+
- `AUTH` - Authentication/Authorization tests
|
|
22
|
+
- `UTIL` - Backend utility function tests
|
|
23
|
+
- `SERIALIZER` - DRF serializers/Pydantic schemas tests
|
|
24
|
+
- `MIDDLEWARE` - Django middleware tests
|
|
25
|
+
|
|
26
|
+
**Type Suffixes:**
|
|
27
|
+
|
|
28
|
+
- `UNIT` - Unit tests (isolated component testing)
|
|
29
|
+
- `INT` - Integration tests (component interaction testing)
|
|
30
|
+
- `E2E` - End-to-end tests (full API workflow testing)
|
|
31
|
+
|
|
32
|
+
**Examples:**
|
|
33
|
+
|
|
34
|
+
- `VIEW-UNIT-001` - First unit test for Django view
|
|
35
|
+
- `MODEL-UNIT-001` - First unit test for Django model
|
|
36
|
+
- `FORM-INT-001` - First integration test for Django form
|
|
37
|
+
- `API-E2E-001` - First end-to-end API test
|
|
38
|
+
|
|
39
|
+
## Testing Framework and Setup
|
|
40
|
+
|
|
41
|
+
### Primary Testing Framework: pytest
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# pytest.ini
|
|
45
|
+
[tool:pytest]
|
|
46
|
+
testpaths = tests
|
|
47
|
+
python_files = test_*.py *_test.py
|
|
48
|
+
python_classes = Test*
|
|
49
|
+
python_functions = test_*
|
|
50
|
+
addopts =
|
|
51
|
+
--strict-markers
|
|
52
|
+
--strict-config
|
|
53
|
+
--cov=src
|
|
54
|
+
--cov-report=term-missing
|
|
55
|
+
--cov-report=html
|
|
56
|
+
--cov-fail-under=80
|
|
57
|
+
markers =
|
|
58
|
+
unit: Unit tests
|
|
59
|
+
integration: Integration tests
|
|
60
|
+
slow: Slow running tests
|
|
61
|
+
api: API endpoint tests
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# conftest.py
|
|
66
|
+
import pytest
|
|
67
|
+
from unittest.mock import Mock
|
|
68
|
+
from src.database import Database
|
|
69
|
+
from src.services.user_service import UserService
|
|
70
|
+
|
|
71
|
+
@pytest.fixture
|
|
72
|
+
def mock_database():
|
|
73
|
+
"""Mock database connection for testing."""
|
|
74
|
+
return Mock(spec=Database)
|
|
75
|
+
|
|
76
|
+
@pytest.fixture
|
|
77
|
+
def user_service(mock_database):
|
|
78
|
+
"""User service with mocked dependencies."""
|
|
79
|
+
return UserService(database=mock_database)
|
|
80
|
+
|
|
81
|
+
@pytest.fixture
|
|
82
|
+
def sample_user_data():
|
|
83
|
+
"""Sample user data for testing."""
|
|
84
|
+
return {
|
|
85
|
+
'name': 'John Doe',
|
|
86
|
+
'email': 'john@example.com',
|
|
87
|
+
'age': 30
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@pytest.fixture(scope="session")
|
|
91
|
+
def test_database():
|
|
92
|
+
"""Session-scoped test database."""
|
|
93
|
+
db = Database(DATABASE_TEST_URL)
|
|
94
|
+
db.create_tables()
|
|
95
|
+
yield db
|
|
96
|
+
db.drop_tables()
|
|
97
|
+
db.close()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Test Case Naming Conventions
|
|
101
|
+
|
|
102
|
+
**Area Prefixes for Python/Django:**
|
|
103
|
+
|
|
104
|
+
- `VIEW` - View/Controller tests
|
|
105
|
+
- `MODEL` - Model/ORM tests
|
|
106
|
+
- `FORM` - Form validation tests
|
|
107
|
+
- `CMD` - Management command tests
|
|
108
|
+
- `API` - API endpoint tests
|
|
109
|
+
- `SRV` - Service layer tests
|
|
110
|
+
- `UTIL` - Utility function tests
|
|
111
|
+
- `DB` - Database operation tests
|
|
112
|
+
- `AUTH` - Authentication/Authorization tests
|
|
113
|
+
- `INT` - Integration tests
|
|
114
|
+
- `E2E` - End-to-end tests
|
|
115
|
+
|
|
116
|
+
**Examples:**
|
|
117
|
+
|
|
118
|
+
- `VIEW-UNIT-001` - First unit test for View layer
|
|
119
|
+
- `MODEL-UNIT-001` - First unit test for Model layer
|
|
120
|
+
- `API-INT-001` - First integration test for API
|
|
121
|
+
- `E2E-FLOW-001` - First end-to-end workflow test
|
|
122
|
+
|
|
123
|
+
## Unit Testing Patterns
|
|
124
|
+
|
|
125
|
+
### Service Layer Testing
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
# tests/unit/test_user_service.py
|
|
129
|
+
import pytest
|
|
130
|
+
from unittest.mock import Mock, patch, call
|
|
131
|
+
from src.services.user_service import UserService, UserNotFoundError
|
|
132
|
+
from src.models.user import User
|
|
133
|
+
|
|
134
|
+
class TestUserService:
|
|
135
|
+
"""Test suite for UserService class."""
|
|
136
|
+
|
|
137
|
+
@pytest.fixture
|
|
138
|
+
def mock_repository(self):
|
|
139
|
+
"""Mock user repository."""
|
|
140
|
+
return Mock()
|
|
141
|
+
|
|
142
|
+
@pytest.fixture
|
|
143
|
+
def user_service(self, mock_repository):
|
|
144
|
+
"""UserService instance with mocked dependencies."""
|
|
145
|
+
return UserService(repository=mock_repository)
|
|
146
|
+
|
|
147
|
+
def test_get_user_by_id_success(self, user_service, mock_repository):
|
|
148
|
+
"""Test successful user retrieval by ID."""
|
|
149
|
+
# Arrange
|
|
150
|
+
user_id = "123"
|
|
151
|
+
expected_user = User(id=user_id, name="John Doe", email="john@example.com")
|
|
152
|
+
mock_repository.find_by_id.return_value = expected_user
|
|
153
|
+
|
|
154
|
+
# Act
|
|
155
|
+
result = user_service.get_user_by_id(user_id)
|
|
156
|
+
|
|
157
|
+
# Assert
|
|
158
|
+
assert result == expected_user
|
|
159
|
+
mock_repository.find_by_id.assert_called_once_with(user_id)
|
|
160
|
+
|
|
161
|
+
def test_get_user_by_id_not_found(self, user_service, mock_repository):
|
|
162
|
+
"""Test user retrieval when user doesn't exist."""
|
|
163
|
+
# Arrange
|
|
164
|
+
user_id = "999"
|
|
165
|
+
mock_repository.find_by_id.return_value = None
|
|
166
|
+
|
|
167
|
+
# Act & Assert
|
|
168
|
+
with pytest.raises(UserNotFoundError, match=f"User {user_id} not found"):
|
|
169
|
+
user_service.get_user_by_id(user_id)
|
|
170
|
+
|
|
171
|
+
mock_repository.find_by_id.assert_called_once_with(user_id)
|
|
172
|
+
|
|
173
|
+
@pytest.mark.parametrize("user_id,expected_calls", [
|
|
174
|
+
("valid_id", 1),
|
|
175
|
+
("another_id", 1),
|
|
176
|
+
])
|
|
177
|
+
def test_get_user_by_id_repository_calls(
|
|
178
|
+
self, user_service, mock_repository, user_id, expected_calls
|
|
179
|
+
):
|
|
180
|
+
"""Test repository is called correct number of times."""
|
|
181
|
+
# Arrange
|
|
182
|
+
mock_repository.find_by_id.return_value = Mock()
|
|
183
|
+
|
|
184
|
+
# Act
|
|
185
|
+
user_service.get_user_by_id(user_id)
|
|
186
|
+
|
|
187
|
+
# Assert
|
|
188
|
+
assert mock_repository.find_by_id.call_count == expected_calls
|
|
189
|
+
|
|
190
|
+
def test_create_user_success(self, user_service, mock_repository):
|
|
191
|
+
"""Test successful user creation."""
|
|
192
|
+
# Arrange
|
|
193
|
+
user_data = {
|
|
194
|
+
'name': 'Jane Doe',
|
|
195
|
+
'email': 'jane@example.com'
|
|
196
|
+
}
|
|
197
|
+
created_user = User(id="456", **user_data)
|
|
198
|
+
mock_repository.create.return_value = created_user
|
|
199
|
+
|
|
200
|
+
# Act
|
|
201
|
+
result = user_service.create_user(user_data)
|
|
202
|
+
|
|
203
|
+
# Assert
|
|
204
|
+
assert result == created_user
|
|
205
|
+
mock_repository.create.assert_called_once_with(user_data)
|
|
206
|
+
|
|
207
|
+
def test_create_user_validation_error(self, user_service):
|
|
208
|
+
"""Test user creation with invalid data."""
|
|
209
|
+
# Arrange
|
|
210
|
+
invalid_data = {'name': ''} # Missing email
|
|
211
|
+
|
|
212
|
+
# Act & Assert
|
|
213
|
+
with pytest.raises(ValueError, match="Invalid user data"):
|
|
214
|
+
user_service.create_user(invalid_data)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Model Testing (Django)
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
# tests/unit/test_user_model.py
|
|
221
|
+
import pytest
|
|
222
|
+
from django.test import TestCase
|
|
223
|
+
from django.core.exceptions import ValidationError
|
|
224
|
+
from django.db import IntegrityError
|
|
225
|
+
from src.models.user import User
|
|
226
|
+
|
|
227
|
+
class TestUserModel(TestCase):
|
|
228
|
+
"""Test suite for User model."""
|
|
229
|
+
|
|
230
|
+
def setUp(self):
|
|
231
|
+
"""Set up test data."""
|
|
232
|
+
self.valid_user_data = {
|
|
233
|
+
'name': 'John Doe',
|
|
234
|
+
'email': 'john@example.com',
|
|
235
|
+
'age': 30
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
def test_create_user_success(self):
|
|
239
|
+
"""Test successful user creation."""
|
|
240
|
+
# Act
|
|
241
|
+
user = User.objects.create(**self.valid_user_data)
|
|
242
|
+
|
|
243
|
+
# Assert
|
|
244
|
+
assert user.pk is not None
|
|
245
|
+
assert user.name == self.valid_user_data['name']
|
|
246
|
+
assert user.email == self.valid_user_data['email']
|
|
247
|
+
assert user.age == self.valid_user_data['age']
|
|
248
|
+
assert user.is_active is True # Default value
|
|
249
|
+
|
|
250
|
+
def test_user_str_representation(self):
|
|
251
|
+
"""Test string representation of user."""
|
|
252
|
+
# Arrange
|
|
253
|
+
user = User(**self.valid_user_data)
|
|
254
|
+
|
|
255
|
+
# Act
|
|
256
|
+
result = str(user)
|
|
257
|
+
|
|
258
|
+
# Assert
|
|
259
|
+
expected = f"{self.valid_user_data['name']} ({self.valid_user_data['email']})"
|
|
260
|
+
assert result == expected
|
|
261
|
+
|
|
262
|
+
def test_email_uniqueness_constraint(self):
|
|
263
|
+
"""Test email uniqueness is enforced."""
|
|
264
|
+
# Arrange
|
|
265
|
+
User.objects.create(**self.valid_user_data)
|
|
266
|
+
|
|
267
|
+
# Act & Assert
|
|
268
|
+
with pytest.raises(IntegrityError):
|
|
269
|
+
User.objects.create(**self.valid_user_data)
|
|
270
|
+
|
|
271
|
+
def test_email_validation(self):
|
|
272
|
+
"""Test email format validation."""
|
|
273
|
+
# Arrange
|
|
274
|
+
invalid_data = self.valid_user_data.copy()
|
|
275
|
+
invalid_data['email'] = 'invalid-email'
|
|
276
|
+
|
|
277
|
+
# Act & Assert
|
|
278
|
+
user = User(**invalid_data)
|
|
279
|
+
with pytest.raises(ValidationError):
|
|
280
|
+
user.full_clean()
|
|
281
|
+
|
|
282
|
+
@pytest.mark.parametrize("age,is_valid", [
|
|
283
|
+
(0, False), # Too young
|
|
284
|
+
(13, True), # Minimum valid age
|
|
285
|
+
(120, True), # Maximum reasonable age
|
|
286
|
+
(150, False), # Too old
|
|
287
|
+
(-1, False), # Negative age
|
|
288
|
+
])
|
|
289
|
+
def test_age_validation(self, age, is_valid):
|
|
290
|
+
"""Test age validation with various values."""
|
|
291
|
+
# Arrange
|
|
292
|
+
user_data = self.valid_user_data.copy()
|
|
293
|
+
user_data['age'] = age
|
|
294
|
+
user = User(**user_data)
|
|
295
|
+
|
|
296
|
+
# Act & Assert
|
|
297
|
+
if is_valid:
|
|
298
|
+
user.full_clean() # Should not raise
|
|
299
|
+
else:
|
|
300
|
+
with pytest.raises(ValidationError):
|
|
301
|
+
user.full_clean()
|
|
302
|
+
|
|
303
|
+
def test_get_full_name_method(self):
|
|
304
|
+
"""Test custom method for getting full name."""
|
|
305
|
+
# Arrange
|
|
306
|
+
user = User(name="John Doe", email="john@example.com")
|
|
307
|
+
|
|
308
|
+
# Act
|
|
309
|
+
result = user.get_full_name()
|
|
310
|
+
|
|
311
|
+
# Assert
|
|
312
|
+
assert result == "John Doe"
|
|
313
|
+
|
|
314
|
+
def test_is_adult_property(self):
|
|
315
|
+
"""Test custom property for checking if user is adult."""
|
|
316
|
+
# Arrange
|
|
317
|
+
adult_user = User(age=25, name="Adult", email="adult@example.com")
|
|
318
|
+
minor_user = User(age=16, name="Minor", email="minor@example.com")
|
|
319
|
+
|
|
320
|
+
# Act & Assert
|
|
321
|
+
assert adult_user.is_adult is True
|
|
322
|
+
assert minor_user.is_adult is False
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### View Testing (Django)
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
# tests/unit/test_user_views.py
|
|
329
|
+
import json
|
|
330
|
+
import pytest
|
|
331
|
+
from django.test import TestCase, Client
|
|
332
|
+
from django.contrib.auth.models import User as AuthUser
|
|
333
|
+
from django.urls import reverse
|
|
334
|
+
from src.models.user import User
|
|
335
|
+
|
|
336
|
+
class TestUserViews(TestCase):
|
|
337
|
+
"""Test suite for user-related views."""
|
|
338
|
+
|
|
339
|
+
def setUp(self):
|
|
340
|
+
"""Set up test data and client."""
|
|
341
|
+
self.client = Client()
|
|
342
|
+
self.auth_user = AuthUser.objects.create_user(
|
|
343
|
+
username='testuser',
|
|
344
|
+
password='testpass123'
|
|
345
|
+
)
|
|
346
|
+
self.user = User.objects.create(
|
|
347
|
+
name='John Doe',
|
|
348
|
+
email='john@example.com',
|
|
349
|
+
age=30
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def test_user_list_view_success(self):
|
|
353
|
+
"""Test user list view returns users."""
|
|
354
|
+
# Act
|
|
355
|
+
response = self.client.get(reverse('user:list'))
|
|
356
|
+
|
|
357
|
+
# Assert
|
|
358
|
+
assert response.status_code == 200
|
|
359
|
+
assert 'users' in response.context
|
|
360
|
+
assert self.user in response.context['users']
|
|
361
|
+
|
|
362
|
+
def test_user_detail_view_success(self):
|
|
363
|
+
"""Test user detail view returns specific user."""
|
|
364
|
+
# Act
|
|
365
|
+
response = self.client.get(
|
|
366
|
+
reverse('user:detail', kwargs={'pk': self.user.pk})
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Assert
|
|
370
|
+
assert response.status_code == 200
|
|
371
|
+
assert response.context['user'] == self.user
|
|
372
|
+
|
|
373
|
+
def test_user_detail_view_not_found(self):
|
|
374
|
+
"""Test user detail view with non-existent user."""
|
|
375
|
+
# Act
|
|
376
|
+
response = self.client.get(
|
|
377
|
+
reverse('user:detail', kwargs={'pk': 999})
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Assert
|
|
381
|
+
assert response.status_code == 404
|
|
382
|
+
|
|
383
|
+
def test_user_create_view_get(self):
|
|
384
|
+
"""Test GET request to user create view."""
|
|
385
|
+
# Arrange
|
|
386
|
+
self.client.login(username='testuser', password='testpass123')
|
|
387
|
+
|
|
388
|
+
# Act
|
|
389
|
+
response = self.client.get(reverse('user:create'))
|
|
390
|
+
|
|
391
|
+
# Assert
|
|
392
|
+
assert response.status_code == 200
|
|
393
|
+
assert 'form' in response.context
|
|
394
|
+
|
|
395
|
+
def test_user_create_view_post_success(self):
|
|
396
|
+
"""Test successful POST to user create view."""
|
|
397
|
+
# Arrange
|
|
398
|
+
self.client.login(username='testuser', password='testpass123')
|
|
399
|
+
user_data = {
|
|
400
|
+
'name': 'Jane Doe',
|
|
401
|
+
'email': 'jane@example.com',
|
|
402
|
+
'age': 25
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
# Act
|
|
406
|
+
response = self.client.post(reverse('user:create'), data=user_data)
|
|
407
|
+
|
|
408
|
+
# Assert
|
|
409
|
+
assert response.status_code == 302 # Redirect after success
|
|
410
|
+
assert User.objects.filter(email='jane@example.com').exists()
|
|
411
|
+
|
|
412
|
+
def test_user_create_view_post_invalid_data(self):
|
|
413
|
+
"""Test POST to user create view with invalid data."""
|
|
414
|
+
# Arrange
|
|
415
|
+
self.client.login(username='testuser', password='testpass123')
|
|
416
|
+
invalid_data = {
|
|
417
|
+
'name': '', # Invalid: empty name
|
|
418
|
+
'email': 'invalid-email', # Invalid: bad email format
|
|
419
|
+
'age': -5 # Invalid: negative age
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# Act
|
|
423
|
+
response = self.client.post(reverse('user:create'), data=invalid_data)
|
|
424
|
+
|
|
425
|
+
# Assert
|
|
426
|
+
assert response.status_code == 200 # Returns form with errors
|
|
427
|
+
assert 'form' in response.context
|
|
428
|
+
assert response.context['form'].errors
|
|
429
|
+
|
|
430
|
+
def test_user_create_view_requires_authentication(self):
|
|
431
|
+
"""Test user create view requires authentication."""
|
|
432
|
+
# Act
|
|
433
|
+
response = self.client.get(reverse('user:create'))
|
|
434
|
+
|
|
435
|
+
# Assert
|
|
436
|
+
assert response.status_code == 302 # Redirect to login
|
|
437
|
+
assert '/login/' in response.url
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### API Testing (FastAPI)
|
|
441
|
+
|
|
442
|
+
```python
|
|
443
|
+
# tests/unit/test_user_api.py
|
|
444
|
+
import pytest
|
|
445
|
+
from fastapi.testclient import TestClient
|
|
446
|
+
from unittest.mock import Mock, patch
|
|
447
|
+
from src.main import app
|
|
448
|
+
from src.services.user_service import UserService
|
|
449
|
+
from src.models.user import User
|
|
450
|
+
|
|
451
|
+
client = TestClient(app)
|
|
452
|
+
|
|
453
|
+
class TestUserAPI:
|
|
454
|
+
"""Test suite for User API endpoints."""
|
|
455
|
+
|
|
456
|
+
@patch('src.dependencies.get_user_service')
|
|
457
|
+
def test_get_user_success(self, mock_get_service):
|
|
458
|
+
"""Test successful user retrieval via API."""
|
|
459
|
+
# Arrange
|
|
460
|
+
user_id = "123"
|
|
461
|
+
expected_user = User(id=user_id, name="John Doe", email="john@example.com")
|
|
462
|
+
mock_service = Mock(spec=UserService)
|
|
463
|
+
mock_service.get_user_by_id.return_value = expected_user
|
|
464
|
+
mock_get_service.return_value = mock_service
|
|
465
|
+
|
|
466
|
+
# Act
|
|
467
|
+
response = client.get(f"/users/{user_id}")
|
|
468
|
+
|
|
469
|
+
# Assert
|
|
470
|
+
assert response.status_code == 200
|
|
471
|
+
assert response.json()["id"] == user_id
|
|
472
|
+
assert response.json()["name"] == "John Doe"
|
|
473
|
+
mock_service.get_user_by_id.assert_called_once_with(user_id)
|
|
474
|
+
|
|
475
|
+
@patch('src.dependencies.get_user_service')
|
|
476
|
+
def test_get_user_not_found(self, mock_get_service):
|
|
477
|
+
"""Test user not found via API."""
|
|
478
|
+
# Arrange
|
|
479
|
+
user_id = "999"
|
|
480
|
+
mock_service = Mock(spec=UserService)
|
|
481
|
+
mock_service.get_user_by_id.side_effect = UserNotFoundError("User not found")
|
|
482
|
+
mock_get_service.return_value = mock_service
|
|
483
|
+
|
|
484
|
+
# Act
|
|
485
|
+
response = client.get(f"/users/{user_id}")
|
|
486
|
+
|
|
487
|
+
# Assert
|
|
488
|
+
assert response.status_code == 404
|
|
489
|
+
assert "not found" in response.json()["detail"].lower()
|
|
490
|
+
|
|
491
|
+
@patch('src.dependencies.get_user_service')
|
|
492
|
+
def test_create_user_success(self, mock_get_service):
|
|
493
|
+
"""Test successful user creation via API."""
|
|
494
|
+
# Arrange
|
|
495
|
+
user_data = {
|
|
496
|
+
"name": "Jane Doe",
|
|
497
|
+
"email": "jane@example.com",
|
|
498
|
+
"age": 25
|
|
499
|
+
}
|
|
500
|
+
created_user = User(id="456", **user_data)
|
|
501
|
+
mock_service = Mock(spec=UserService)
|
|
502
|
+
mock_service.create_user.return_value = created_user
|
|
503
|
+
mock_get_service.return_value = mock_service
|
|
504
|
+
|
|
505
|
+
# Act
|
|
506
|
+
response = client.post("/users/", json=user_data)
|
|
507
|
+
|
|
508
|
+
# Assert
|
|
509
|
+
assert response.status_code == 201
|
|
510
|
+
assert response.json()["id"] == "456"
|
|
511
|
+
assert response.json()["name"] == user_data["name"]
|
|
512
|
+
mock_service.create_user.assert_called_once_with(user_data)
|
|
513
|
+
|
|
514
|
+
def test_create_user_validation_error(self):
|
|
515
|
+
"""Test user creation with invalid data."""
|
|
516
|
+
# Arrange
|
|
517
|
+
invalid_data = {
|
|
518
|
+
"name": "", # Invalid: empty name
|
|
519
|
+
"email": "invalid-email", # Invalid: bad format
|
|
520
|
+
"age": -5 # Invalid: negative age
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
# Act
|
|
524
|
+
response = client.post("/users/", json=invalid_data)
|
|
525
|
+
|
|
526
|
+
# Assert
|
|
527
|
+
assert response.status_code == 422
|
|
528
|
+
assert "detail" in response.json()
|
|
529
|
+
|
|
530
|
+
@pytest.mark.parametrize("user_data,expected_status", [
|
|
531
|
+
({"name": "Valid User", "email": "valid@example.com", "age": 25}, 201),
|
|
532
|
+
({"name": "", "email": "valid@example.com", "age": 25}, 422),
|
|
533
|
+
({"name": "Valid User", "email": "invalid-email", "age": 25}, 422),
|
|
534
|
+
({"name": "Valid User", "email": "valid@example.com", "age": -1}, 422),
|
|
535
|
+
])
|
|
536
|
+
@patch('src.dependencies.get_user_service')
|
|
537
|
+
def test_create_user_various_data(
|
|
538
|
+
self, mock_get_service, user_data, expected_status
|
|
539
|
+
):
|
|
540
|
+
"""Test user creation with various data combinations."""
|
|
541
|
+
# Arrange
|
|
542
|
+
if expected_status == 201:
|
|
543
|
+
created_user = User(id="123", **user_data)
|
|
544
|
+
mock_service = Mock(spec=UserService)
|
|
545
|
+
mock_service.create_user.return_value = created_user
|
|
546
|
+
mock_get_service.return_value = mock_service
|
|
547
|
+
|
|
548
|
+
# Act
|
|
549
|
+
response = client.post("/users/", json=user_data)
|
|
550
|
+
|
|
551
|
+
# Assert
|
|
552
|
+
assert response.status_code == expected_status
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
## Integration Testing
|
|
556
|
+
|
|
557
|
+
### Database Integration Testing
|
|
558
|
+
|
|
559
|
+
```python
|
|
560
|
+
# tests/integration/test_user_repository.py
|
|
561
|
+
import pytest
|
|
562
|
+
import asyncio
|
|
563
|
+
from sqlalchemy import create_engine
|
|
564
|
+
from sqlalchemy.orm import sessionmaker
|
|
565
|
+
from src.database.base import Base
|
|
566
|
+
from src.repositories.user_repository import UserRepository
|
|
567
|
+
from src.models.user import User
|
|
568
|
+
|
|
569
|
+
class TestUserRepositoryIntegration:
|
|
570
|
+
"""Integration tests for UserRepository with real database."""
|
|
571
|
+
|
|
572
|
+
@pytest.fixture(scope="class")
|
|
573
|
+
def db_engine(self):
|
|
574
|
+
"""Create test database engine."""
|
|
575
|
+
engine = create_engine("sqlite:///:memory:", echo=False)
|
|
576
|
+
Base.metadata.create_all(engine)
|
|
577
|
+
yield engine
|
|
578
|
+
Base.metadata.drop_all(engine)
|
|
579
|
+
|
|
580
|
+
@pytest.fixture
|
|
581
|
+
def db_session(self, db_engine):
|
|
582
|
+
"""Create database session for each test."""
|
|
583
|
+
Session = sessionmaker(bind=db_engine)
|
|
584
|
+
session = Session()
|
|
585
|
+
yield session
|
|
586
|
+
session.rollback()
|
|
587
|
+
session.close()
|
|
588
|
+
|
|
589
|
+
@pytest.fixture
|
|
590
|
+
def user_repository(self, db_session):
|
|
591
|
+
"""Create UserRepository with real database session."""
|
|
592
|
+
return UserRepository(session=db_session)
|
|
593
|
+
|
|
594
|
+
def test_create_and_find_user(self, user_repository):
|
|
595
|
+
"""Test creating and finding a user in database."""
|
|
596
|
+
# Arrange
|
|
597
|
+
user_data = {
|
|
598
|
+
'name': 'John Doe',
|
|
599
|
+
'email': 'john@example.com',
|
|
600
|
+
'age': 30
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
# Act
|
|
604
|
+
created_user = user_repository.create(user_data)
|
|
605
|
+
found_user = user_repository.find_by_id(created_user.id)
|
|
606
|
+
|
|
607
|
+
# Assert
|
|
608
|
+
assert found_user is not None
|
|
609
|
+
assert found_user.id == created_user.id
|
|
610
|
+
assert found_user.name == user_data['name']
|
|
611
|
+
assert found_user.email == user_data['email']
|
|
612
|
+
|
|
613
|
+
def test_find_by_email(self, user_repository):
|
|
614
|
+
"""Test finding user by email."""
|
|
615
|
+
# Arrange
|
|
616
|
+
user_data = {
|
|
617
|
+
'name': 'Jane Doe',
|
|
618
|
+
'email': 'jane@example.com',
|
|
619
|
+
'age': 25
|
|
620
|
+
}
|
|
621
|
+
created_user = user_repository.create(user_data)
|
|
622
|
+
|
|
623
|
+
# Act
|
|
624
|
+
found_user = user_repository.find_by_email('jane@example.com')
|
|
625
|
+
|
|
626
|
+
# Assert
|
|
627
|
+
assert found_user is not None
|
|
628
|
+
assert found_user.id == created_user.id
|
|
629
|
+
assert found_user.email == 'jane@example.com'
|
|
630
|
+
|
|
631
|
+
def test_update_user(self, user_repository):
|
|
632
|
+
"""Test updating user information."""
|
|
633
|
+
# Arrange
|
|
634
|
+
user_data = {
|
|
635
|
+
'name': 'Original Name',
|
|
636
|
+
'email': 'original@example.com',
|
|
637
|
+
'age': 30
|
|
638
|
+
}
|
|
639
|
+
created_user = user_repository.create(user_data)
|
|
640
|
+
|
|
641
|
+
# Act
|
|
642
|
+
update_data = {'name': 'Updated Name', 'age': 31}
|
|
643
|
+
updated_user = user_repository.update(created_user.id, update_data)
|
|
644
|
+
|
|
645
|
+
# Assert
|
|
646
|
+
assert updated_user.name == 'Updated Name'
|
|
647
|
+
assert updated_user.age == 31
|
|
648
|
+
assert updated_user.email == 'original@example.com' # Unchanged
|
|
649
|
+
|
|
650
|
+
def test_delete_user(self, user_repository):
|
|
651
|
+
"""Test deleting a user."""
|
|
652
|
+
# Arrange
|
|
653
|
+
user_data = {
|
|
654
|
+
'name': 'To Be Deleted',
|
|
655
|
+
'email': 'delete@example.com',
|
|
656
|
+
'age': 25
|
|
657
|
+
}
|
|
658
|
+
created_user = user_repository.create(user_data)
|
|
659
|
+
|
|
660
|
+
# Act
|
|
661
|
+
result = user_repository.delete(created_user.id)
|
|
662
|
+
found_user = user_repository.find_by_id(created_user.id)
|
|
663
|
+
|
|
664
|
+
# Assert
|
|
665
|
+
assert result is True
|
|
666
|
+
assert found_user is None
|
|
667
|
+
|
|
668
|
+
def test_list_users_with_pagination(self, user_repository):
|
|
669
|
+
"""Test listing users with pagination."""
|
|
670
|
+
# Arrange
|
|
671
|
+
users_data = [
|
|
672
|
+
{'name': f'User {i}', 'email': f'user{i}@example.com', 'age': 20 + i}
|
|
673
|
+
for i in range(5)
|
|
674
|
+
]
|
|
675
|
+
created_users = [user_repository.create(data) for data in users_data]
|
|
676
|
+
|
|
677
|
+
# Act
|
|
678
|
+
page_1 = user_repository.list_users(page=1, per_page=2)
|
|
679
|
+
page_2 = user_repository.list_users(page=2, per_page=2)
|
|
680
|
+
|
|
681
|
+
# Assert
|
|
682
|
+
assert len(page_1) == 2
|
|
683
|
+
assert len(page_2) == 2
|
|
684
|
+
assert page_1[0].id != page_2[0].id # Different users
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### API Integration Testing
|
|
688
|
+
|
|
689
|
+
```python
|
|
690
|
+
# tests/integration/test_api_integration.py
|
|
691
|
+
import pytest
|
|
692
|
+
from fastapi.testclient import TestClient
|
|
693
|
+
from sqlalchemy import create_engine
|
|
694
|
+
from sqlalchemy.orm import sessionmaker
|
|
695
|
+
from src.main import app
|
|
696
|
+
from src.database.base import Base
|
|
697
|
+
from src.dependencies import get_database_session
|
|
698
|
+
|
|
699
|
+
class TestAPIIntegration:
|
|
700
|
+
"""Integration tests for API with real database."""
|
|
701
|
+
|
|
702
|
+
@pytest.fixture(scope="class")
|
|
703
|
+
def test_engine(self):
|
|
704
|
+
"""Create test database engine."""
|
|
705
|
+
engine = create_engine("sqlite:///:memory:", echo=False)
|
|
706
|
+
Base.metadata.create_all(engine)
|
|
707
|
+
yield engine
|
|
708
|
+
Base.metadata.drop_all(engine)
|
|
709
|
+
|
|
710
|
+
@pytest.fixture
|
|
711
|
+
def test_session(self, test_engine):
|
|
712
|
+
"""Create test database session."""
|
|
713
|
+
Session = sessionmaker(bind=test_engine)
|
|
714
|
+
session = Session()
|
|
715
|
+
yield session
|
|
716
|
+
session.close()
|
|
717
|
+
|
|
718
|
+
@pytest.fixture
|
|
719
|
+
def client(self, test_session):
|
|
720
|
+
"""Create test client with test database."""
|
|
721
|
+
def override_get_db():
|
|
722
|
+
yield test_session
|
|
723
|
+
|
|
724
|
+
app.dependency_overrides[get_database_session] = override_get_db
|
|
725
|
+
client = TestClient(app)
|
|
726
|
+
yield client
|
|
727
|
+
app.dependency_overrides.clear()
|
|
728
|
+
|
|
729
|
+
def test_full_user_crud_workflow(self, client):
|
|
730
|
+
"""Test complete CRUD workflow for users."""
|
|
731
|
+
# Create user
|
|
732
|
+
user_data = {
|
|
733
|
+
"name": "Test User",
|
|
734
|
+
"email": "test@example.com",
|
|
735
|
+
"age": 30
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
create_response = client.post("/users/", json=user_data)
|
|
739
|
+
assert create_response.status_code == 201
|
|
740
|
+
created_user = create_response.json()
|
|
741
|
+
user_id = created_user["id"]
|
|
742
|
+
|
|
743
|
+
# Read user
|
|
744
|
+
get_response = client.get(f"/users/{user_id}")
|
|
745
|
+
assert get_response.status_code == 200
|
|
746
|
+
assert get_response.json()["name"] == user_data["name"]
|
|
747
|
+
|
|
748
|
+
# Update user
|
|
749
|
+
update_data = {"name": "Updated User", "age": 31}
|
|
750
|
+
update_response = client.patch(f"/users/{user_id}", json=update_data)
|
|
751
|
+
assert update_response.status_code == 200
|
|
752
|
+
assert update_response.json()["name"] == "Updated User"
|
|
753
|
+
|
|
754
|
+
# List users
|
|
755
|
+
list_response = client.get("/users/")
|
|
756
|
+
assert list_response.status_code == 200
|
|
757
|
+
users = list_response.json()
|
|
758
|
+
assert len(users) >= 1
|
|
759
|
+
assert any(user["id"] == user_id for user in users)
|
|
760
|
+
|
|
761
|
+
# Delete user
|
|
762
|
+
delete_response = client.delete(f"/users/{user_id}")
|
|
763
|
+
assert delete_response.status_code == 204
|
|
764
|
+
|
|
765
|
+
# Verify deletion
|
|
766
|
+
get_deleted_response = client.get(f"/users/{user_id}")
|
|
767
|
+
assert get_deleted_response.status_code == 404
|
|
768
|
+
|
|
769
|
+
def test_user_email_uniqueness_constraint(self, client):
|
|
770
|
+
"""Test email uniqueness is enforced at API level."""
|
|
771
|
+
# Arrange
|
|
772
|
+
user_data = {
|
|
773
|
+
"name": "First User",
|
|
774
|
+
"email": "duplicate@example.com",
|
|
775
|
+
"age": 25
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
# Act - Create first user
|
|
779
|
+
first_response = client.post("/users/", json=user_data)
|
|
780
|
+
assert first_response.status_code == 201
|
|
781
|
+
|
|
782
|
+
# Act - Try to create second user with same email
|
|
783
|
+
user_data["name"] = "Second User"
|
|
784
|
+
second_response = client.post("/users/", json=user_data)
|
|
785
|
+
|
|
786
|
+
# Assert
|
|
787
|
+
assert second_response.status_code == 400
|
|
788
|
+
assert "email" in second_response.json()["detail"].lower()
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
## Async Testing
|
|
792
|
+
|
|
793
|
+
### Async Function Testing
|
|
794
|
+
|
|
795
|
+
```python
|
|
796
|
+
# tests/unit/test_async_service.py
|
|
797
|
+
import pytest
|
|
798
|
+
import asyncio
|
|
799
|
+
from unittest.mock import AsyncMock, Mock
|
|
800
|
+
from src.services.async_user_service import AsyncUserService
|
|
801
|
+
|
|
802
|
+
class TestAsyncUserService:
|
|
803
|
+
"""Test suite for async user service."""
|
|
804
|
+
|
|
805
|
+
@pytest.fixture
|
|
806
|
+
def mock_async_repository(self):
|
|
807
|
+
"""Mock async repository."""
|
|
808
|
+
mock = AsyncMock()
|
|
809
|
+
return mock
|
|
810
|
+
|
|
811
|
+
@pytest.fixture
|
|
812
|
+
def async_user_service(self, mock_async_repository):
|
|
813
|
+
"""Async user service with mocked dependencies."""
|
|
814
|
+
return AsyncUserService(repository=mock_async_repository)
|
|
815
|
+
|
|
816
|
+
@pytest.mark.asyncio
|
|
817
|
+
async def test_get_user_by_id_async(self, async_user_service, mock_async_repository):
|
|
818
|
+
"""Test async user retrieval."""
|
|
819
|
+
# Arrange
|
|
820
|
+
user_id = "123"
|
|
821
|
+
expected_user = {"id": user_id, "name": "John Doe"}
|
|
822
|
+
mock_async_repository.find_by_id.return_value = expected_user
|
|
823
|
+
|
|
824
|
+
# Act
|
|
825
|
+
result = await async_user_service.get_user_by_id(user_id)
|
|
826
|
+
|
|
827
|
+
# Assert
|
|
828
|
+
assert result == expected_user
|
|
829
|
+
mock_async_repository.find_by_id.assert_called_once_with(user_id)
|
|
830
|
+
|
|
831
|
+
@pytest.mark.asyncio
|
|
832
|
+
async def test_create_multiple_users_concurrently(
|
|
833
|
+
self, async_user_service, mock_async_repository
|
|
834
|
+
):
|
|
835
|
+
"""Test creating multiple users concurrently."""
|
|
836
|
+
# Arrange
|
|
837
|
+
users_data = [
|
|
838
|
+
{"name": f"User {i}", "email": f"user{i}@example.com"}
|
|
839
|
+
for i in range(3)
|
|
840
|
+
]
|
|
841
|
+
mock_async_repository.create.side_effect = [
|
|
842
|
+
{"id": f"id_{i}", **data} for i, data in enumerate(users_data)
|
|
843
|
+
]
|
|
844
|
+
|
|
845
|
+
# Act
|
|
846
|
+
tasks = [
|
|
847
|
+
async_user_service.create_user(data) for data in users_data
|
|
848
|
+
]
|
|
849
|
+
results = await asyncio.gather(*tasks)
|
|
850
|
+
|
|
851
|
+
# Assert
|
|
852
|
+
assert len(results) == 3
|
|
853
|
+
assert all("id" in result for result in results)
|
|
854
|
+
assert mock_async_repository.create.call_count == 3
|
|
855
|
+
|
|
856
|
+
@pytest.mark.asyncio
|
|
857
|
+
async def test_timeout_handling(self, async_user_service, mock_async_repository):
|
|
858
|
+
"""Test handling of timeout in async operations."""
|
|
859
|
+
# Arrange
|
|
860
|
+
async def slow_operation(*args, **kwargs):
|
|
861
|
+
await asyncio.sleep(2) # Simulate slow operation
|
|
862
|
+
return {"id": "123", "name": "Slow User"}
|
|
863
|
+
|
|
864
|
+
mock_async_repository.find_by_id.side_effect = slow_operation
|
|
865
|
+
|
|
866
|
+
# Act & Assert
|
|
867
|
+
with pytest.raises(asyncio.TimeoutError):
|
|
868
|
+
await asyncio.wait_for(
|
|
869
|
+
async_user_service.get_user_by_id("123"),
|
|
870
|
+
timeout=1.0
|
|
871
|
+
)
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
## Mocking and Fixtures
|
|
875
|
+
|
|
876
|
+
### Complex Fixture Patterns
|
|
877
|
+
|
|
878
|
+
```python
|
|
879
|
+
# tests/conftest.py
|
|
880
|
+
import pytest
|
|
881
|
+
from unittest.mock import Mock, patch
|
|
882
|
+
from datetime import datetime, timedelta
|
|
883
|
+
from src.models.user import User
|
|
884
|
+
from src.services.email_service import EmailService
|
|
885
|
+
|
|
886
|
+
@pytest.fixture
|
|
887
|
+
def freeze_time():
|
|
888
|
+
"""Fixture to freeze time for consistent testing."""
|
|
889
|
+
frozen_time = datetime(2023, 1, 1, 12, 0, 0)
|
|
890
|
+
with patch('src.utils.datetime.datetime') as mock_datetime:
|
|
891
|
+
mock_datetime.now.return_value = frozen_time
|
|
892
|
+
mock_datetime.utcnow.return_value = frozen_time
|
|
893
|
+
yield frozen_time
|
|
894
|
+
|
|
895
|
+
@pytest.fixture
|
|
896
|
+
def mock_email_service():
|
|
897
|
+
"""Mock email service for testing."""
|
|
898
|
+
mock = Mock(spec=EmailService)
|
|
899
|
+
mock.send_email.return_value = True
|
|
900
|
+
mock.send_bulk_email.return_value = {"sent": 5, "failed": 0}
|
|
901
|
+
return mock
|
|
902
|
+
|
|
903
|
+
@pytest.fixture(params=[
|
|
904
|
+
{"name": "John Doe", "age": 30, "email": "john@example.com"},
|
|
905
|
+
{"name": "Jane Smith", "age": 25, "email": "jane@example.com"},
|
|
906
|
+
])
|
|
907
|
+
def user_data_variations(request):
|
|
908
|
+
"""Parametrized fixture for different user data."""
|
|
909
|
+
return request.param
|
|
910
|
+
|
|
911
|
+
@pytest.fixture
|
|
912
|
+
def users_factory():
|
|
913
|
+
"""Factory fixture for creating multiple users."""
|
|
914
|
+
def _create_users(count=3, **overrides):
|
|
915
|
+
users = []
|
|
916
|
+
for i in range(count):
|
|
917
|
+
user_data = {
|
|
918
|
+
"name": f"User {i}",
|
|
919
|
+
"email": f"user{i}@example.com",
|
|
920
|
+
"age": 20 + i,
|
|
921
|
+
**overrides
|
|
922
|
+
}
|
|
923
|
+
users.append(User(**user_data))
|
|
924
|
+
return users
|
|
925
|
+
return _create_users
|
|
926
|
+
|
|
927
|
+
@pytest.fixture(scope="session")
|
|
928
|
+
def test_config():
|
|
929
|
+
"""Session-scoped configuration for tests."""
|
|
930
|
+
return {
|
|
931
|
+
"database_url": "sqlite:///:memory:",
|
|
932
|
+
"debug": True,
|
|
933
|
+
"testing": True,
|
|
934
|
+
"email_backend": "mock"
|
|
935
|
+
}
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
### Advanced Mocking Patterns
|
|
939
|
+
|
|
940
|
+
```python
|
|
941
|
+
# tests/unit/test_advanced_mocking.py
|
|
942
|
+
import pytest
|
|
943
|
+
from unittest.mock import Mock, patch, MagicMock, call
|
|
944
|
+
from datetime import datetime
|
|
945
|
+
from src.services.notification_service import NotificationService
|
|
946
|
+
|
|
947
|
+
class TestAdvancedMocking:
|
|
948
|
+
"""Test suite demonstrating advanced mocking patterns."""
|
|
949
|
+
|
|
950
|
+
def test_property_mocking(self):
|
|
951
|
+
"""Test mocking object properties."""
|
|
952
|
+
# Arrange
|
|
953
|
+
mock_user = Mock()
|
|
954
|
+
type(mock_user).is_active = PropertyMock(return_value=True)
|
|
955
|
+
|
|
956
|
+
# Act & Assert
|
|
957
|
+
assert mock_user.is_active is True
|
|
958
|
+
|
|
959
|
+
def test_context_manager_mocking(self):
|
|
960
|
+
"""Test mocking context managers."""
|
|
961
|
+
# Arrange
|
|
962
|
+
with patch('builtins.open', mock_open(read_data='file content')) as mock_file:
|
|
963
|
+
# Act
|
|
964
|
+
with open('test.txt', 'r') as f:
|
|
965
|
+
content = f.read()
|
|
966
|
+
|
|
967
|
+
# Assert
|
|
968
|
+
assert content == 'file content'
|
|
969
|
+
mock_file.assert_called_once_with('test.txt', 'r')
|
|
970
|
+
|
|
971
|
+
def test_side_effect_with_exception(self):
|
|
972
|
+
"""Test using side_effect to raise exceptions."""
|
|
973
|
+
# Arrange
|
|
974
|
+
mock_service = Mock()
|
|
975
|
+
mock_service.get_user.side_effect = [
|
|
976
|
+
{"id": "1", "name": "User 1"}, # First call succeeds
|
|
977
|
+
Exception("Service unavailable"), # Second call fails
|
|
978
|
+
{"id": "3", "name": "User 3"}, # Third call succeeds
|
|
979
|
+
]
|
|
980
|
+
|
|
981
|
+
# Act & Assert
|
|
982
|
+
result1 = mock_service.get_user("1")
|
|
983
|
+
assert result1["name"] == "User 1"
|
|
984
|
+
|
|
985
|
+
with pytest.raises(Exception, match="Service unavailable"):
|
|
986
|
+
mock_service.get_user("2")
|
|
987
|
+
|
|
988
|
+
result3 = mock_service.get_user("3")
|
|
989
|
+
assert result3["name"] == "User 3"
|
|
990
|
+
|
|
991
|
+
def test_mock_chain_calls(self):
|
|
992
|
+
"""Test mocking chained method calls."""
|
|
993
|
+
# Arrange
|
|
994
|
+
mock_database = Mock()
|
|
995
|
+
mock_database.users.filter.return_value.order_by.return_value.limit.return_value = [
|
|
996
|
+
{"id": "1", "name": "User 1"}
|
|
997
|
+
]
|
|
998
|
+
|
|
999
|
+
# Act
|
|
1000
|
+
result = mock_database.users.filter(active=True).order_by('name').limit(10)
|
|
1001
|
+
|
|
1002
|
+
# Assert
|
|
1003
|
+
assert len(result) == 1
|
|
1004
|
+
assert result[0]["name"] == "User 1"
|
|
1005
|
+
|
|
1006
|
+
@patch('src.services.notification_service.EmailService')
|
|
1007
|
+
@patch('src.services.notification_service.SMSService')
|
|
1008
|
+
def test_multiple_patches(self, mock_sms_service, mock_email_service):
|
|
1009
|
+
"""Test using multiple patches."""
|
|
1010
|
+
# Arrange
|
|
1011
|
+
notification_service = NotificationService()
|
|
1012
|
+
mock_email_service.return_value.send.return_value = True
|
|
1013
|
+
mock_sms_service.return_value.send.return_value = True
|
|
1014
|
+
|
|
1015
|
+
# Act
|
|
1016
|
+
result = notification_service.send_notification(
|
|
1017
|
+
"user@example.com",
|
|
1018
|
+
"Test message",
|
|
1019
|
+
channels=["email", "sms"]
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
# Assert
|
|
1023
|
+
assert result is True
|
|
1024
|
+
mock_email_service.return_value.send.assert_called_once()
|
|
1025
|
+
mock_sms_service.return_value.send.assert_called_once()
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
## Performance and Load Testing
|
|
1029
|
+
|
|
1030
|
+
### Performance Testing with pytest-benchmark
|
|
1031
|
+
|
|
1032
|
+
```python
|
|
1033
|
+
# tests/performance/test_user_performance.py
|
|
1034
|
+
import pytest
|
|
1035
|
+
from src.services.user_service import UserService
|
|
1036
|
+
from src.utils.data_processor import DataProcessor
|
|
1037
|
+
|
|
1038
|
+
class TestUserServicePerformance:
|
|
1039
|
+
"""Performance tests for user service operations."""
|
|
1040
|
+
|
|
1041
|
+
@pytest.fixture
|
|
1042
|
+
def large_user_dataset(self):
|
|
1043
|
+
"""Generate large dataset for performance testing."""
|
|
1044
|
+
return [
|
|
1045
|
+
{"name": f"User {i}", "email": f"user{i}@example.com", "age": 20 + (i % 50)}
|
|
1046
|
+
for i in range(1000)
|
|
1047
|
+
]
|
|
1048
|
+
|
|
1049
|
+
def test_user_creation_performance(self, benchmark, user_service, large_user_dataset):
|
|
1050
|
+
"""Benchmark user creation performance."""
|
|
1051
|
+
# Act & Assert
|
|
1052
|
+
result = benchmark(user_service.create_bulk_users, large_user_dataset)
|
|
1053
|
+
assert len(result) == 1000
|
|
1054
|
+
|
|
1055
|
+
def test_user_search_performance(self, benchmark, user_service):
|
|
1056
|
+
"""Benchmark user search performance."""
|
|
1057
|
+
# Arrange
|
|
1058
|
+
# Assume users are already created in database
|
|
1059
|
+
|
|
1060
|
+
# Act & Assert
|
|
1061
|
+
result = benchmark(user_service.search_users, query="User", limit=100)
|
|
1062
|
+
assert len(result) <= 100
|
|
1063
|
+
|
|
1064
|
+
@pytest.mark.parametrize("batch_size", [100, 500, 1000])
|
|
1065
|
+
def test_batch_processing_performance(self, benchmark, batch_size):
|
|
1066
|
+
"""Test performance with different batch sizes."""
|
|
1067
|
+
# Arrange
|
|
1068
|
+
processor = DataProcessor(batch_size=batch_size)
|
|
1069
|
+
data = list(range(10000))
|
|
1070
|
+
|
|
1071
|
+
# Act & Assert
|
|
1072
|
+
result = benchmark(processor.process_batch, data)
|
|
1073
|
+
assert len(result) == 10000
|
|
1074
|
+
```
|