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