omgkit 2.2.0 → 2.3.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 (60) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/plugin/skills/databases/database-management/SKILL.md +288 -0
  4. package/plugin/skills/databases/database-migration/SKILL.md +285 -0
  5. package/plugin/skills/databases/database-schema-design/SKILL.md +195 -0
  6. package/plugin/skills/databases/mongodb/SKILL.md +60 -776
  7. package/plugin/skills/databases/prisma/SKILL.md +53 -744
  8. package/plugin/skills/databases/redis/SKILL.md +53 -860
  9. package/plugin/skills/databases/supabase/SKILL.md +283 -0
  10. package/plugin/skills/devops/aws/SKILL.md +68 -672
  11. package/plugin/skills/devops/github-actions/SKILL.md +54 -657
  12. package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
  13. package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
  14. package/plugin/skills/frameworks/django/SKILL.md +87 -853
  15. package/plugin/skills/frameworks/express/SKILL.md +95 -1301
  16. package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
  17. package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
  18. package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
  19. package/plugin/skills/frameworks/react/SKILL.md +94 -962
  20. package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
  21. package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
  22. package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
  23. package/plugin/skills/frontend/responsive/SKILL.md +76 -799
  24. package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
  25. package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
  26. package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
  27. package/plugin/skills/languages/javascript/SKILL.md +106 -849
  28. package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
  29. package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
  30. package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
  31. package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
  32. package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
  33. package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
  34. package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
  35. package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
  36. package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
  37. package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
  38. package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
  39. package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
  40. package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
  41. package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
  42. package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
  43. package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
  44. package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
  45. package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
  46. package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
  47. package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
  48. package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
  49. package/plugin/skills/security/better-auth/SKILL.md +46 -1034
  50. package/plugin/skills/security/oauth/SKILL.md +80 -934
  51. package/plugin/skills/security/owasp/SKILL.md +78 -862
  52. package/plugin/skills/testing/playwright/SKILL.md +77 -700
  53. package/plugin/skills/testing/pytest/SKILL.md +73 -811
  54. package/plugin/skills/testing/vitest/SKILL.md +60 -920
  55. package/plugin/skills/tools/document-processing/SKILL.md +111 -838
  56. package/plugin/skills/tools/image-processing/SKILL.md +126 -659
  57. package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
  58. package/plugin/skills/tools/media-processing/SKILL.md +118 -735
  59. package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
  60. package/plugin/skills/SKILL_STANDARDS.md +0 -743
@@ -1,890 +1,153 @@
1
1
  ---
2
- name: pytest
3
- description: Python testing with pytest including fixtures, parametrization, mocking, async testing, and CI integration
4
- category: testing
5
- triggers:
6
- - pytest
7
- - python testing
8
- - unit testing python
9
- - fixtures
10
- - test automation
2
+ name: Testing with Pytest
3
+ description: Claude writes comprehensive Python tests using pytest. Use when writing unit tests, creating fixtures, parametrizing tests, mocking dependencies, testing async code, or setting up CI test pipelines.
11
4
  ---
12
5
 
13
- # Pytest
6
+ # Testing with Pytest
14
7
 
15
- Enterprise-grade **Python testing framework** following industry best practices. This skill covers fixtures, parametrization, mocking, async testing, markers, plugins, and CI integration patterns used by top engineering teams.
16
-
17
- ## Purpose
18
-
19
- Build comprehensive Python test suites:
20
-
21
- - Write clear and maintainable unit tests
22
- - Create reusable fixtures for test setup
23
- - Implement parametrized tests for edge cases
24
- - Mock external dependencies effectively
25
- - Test async code with pytest-asyncio
26
- - Generate coverage reports
27
- - Integrate with CI/CD pipelines
28
-
29
- ## Features
30
-
31
- ### 1. Configuration Setup
8
+ ## Quick Start
32
9
 
33
10
  ```ini
34
11
  # pytest.ini
35
12
  [pytest]
36
13
  testpaths = tests
37
- python_files = test_*.py *_test.py
38
- python_classes = Test*
39
- python_functions = test_*
40
- addopts =
41
- -v
42
- --strict-markers
43
- --tb=short
44
- -ra
45
- --cov=src
46
- --cov-report=term-missing
47
- --cov-report=html:coverage_html
48
- --cov-fail-under=80
14
+ python_files = test_*.py
15
+ addopts = -v --strict-markers --cov=src --cov-report=term-missing --cov-fail-under=80
49
16
  markers =
50
- slow: marks tests as slow (deselect with '-m "not slow"')
17
+ slow: marks tests as slow
51
18
  integration: marks tests as integration tests
52
- unit: marks tests as unit tests
53
- smoke: marks tests as smoke tests
54
- filterwarnings =
55
- error
56
- ignore::DeprecationWarning
57
19
  asyncio_mode = auto
58
20
  ```
59
21
 
60
- ```toml
61
- # pyproject.toml
62
- [tool.pytest.ini_options]
63
- testpaths = ["tests"]
64
- python_files = ["test_*.py"]
65
- addopts = "-v --strict-markers --tb=short"
66
- markers = [
67
- "slow: marks tests as slow",
68
- "integration: marks tests as integration tests",
69
- ]
70
-
71
- [tool.coverage.run]
72
- source = ["src"]
73
- branch = true
74
- omit = ["*/tests/*", "*/__init__.py"]
75
-
76
- [tool.coverage.report]
77
- exclude_lines = [
78
- "pragma: no cover",
79
- "def __repr__",
80
- "raise NotImplementedError",
81
- "if TYPE_CHECKING:",
82
- ]
83
- fail_under = 80
84
- show_missing = true
85
- ```
86
-
87
- ### 2. Fixture Patterns
88
-
89
22
  ```python
90
23
  # tests/conftest.py
91
- from typing import Generator, AsyncGenerator
92
- from collections.abc import Iterator
93
24
  import pytest
94
- from unittest.mock import MagicMock, AsyncMock
95
- from sqlalchemy import create_engine
96
- from sqlalchemy.orm import Session, sessionmaker
97
- from httpx import AsyncClient
98
-
99
- from src.database import Base
100
- from src.main import app
101
- from src.models import User, Organization
102
-
103
-
104
- # Database fixtures
105
- @pytest.fixture(scope="session")
106
- def engine():
107
- """Create test database engine."""
108
- engine = create_engine(
109
- "postgresql://test:test@localhost:5432/test_db",
110
- echo=False,
111
- )
112
- Base.metadata.create_all(engine)
113
- yield engine
114
- Base.metadata.drop_all(engine)
115
-
25
+ from sqlalchemy.orm import Session
116
26
 
117
27
  @pytest.fixture
118
- def db_session(engine) -> Generator[Session, None, None]:
28
+ def db_session(engine) -> Session:
119
29
  """Create database session with automatic rollback."""
120
30
  connection = engine.connect()
121
31
  transaction = connection.begin()
122
32
  session = sessionmaker(bind=connection)()
123
-
124
33
  yield session
125
-
126
34
  session.close()
127
35
  transaction.rollback()
128
36
  connection.close()
37
+ ```
129
38
 
39
+ ## Features
130
40
 
131
- # Factory fixtures
132
- @pytest.fixture
133
- def user_factory(db_session: Session):
134
- """Factory for creating test users."""
135
- def _create_user(
136
- email: str = "test@example.com",
137
- name: str = "Test User",
138
- is_active: bool = True,
139
- **kwargs,
140
- ) -> User:
141
- user = User(
142
- email=email,
143
- name=name,
144
- is_active=is_active,
145
- **kwargs,
146
- )
147
- db_session.add(user)
148
- db_session.commit()
149
- db_session.refresh(user)
150
- return user
151
-
152
- return _create_user
153
-
154
-
155
- @pytest.fixture
156
- def test_user(user_factory) -> User:
157
- """Create a standard test user."""
158
- return user_factory(
159
- email="testuser@example.com",
160
- name="Test User",
161
- )
162
-
163
-
164
- @pytest.fixture
165
- def admin_user(user_factory) -> User:
166
- """Create an admin test user."""
167
- return user_factory(
168
- email="admin@example.com",
169
- name="Admin User",
170
- is_admin=True,
171
- )
172
-
173
-
174
- # Mock fixtures
175
- @pytest.fixture
176
- def mock_email_service() -> MagicMock:
177
- """Mock email service."""
178
- mock = MagicMock()
179
- mock.send_email.return_value = {"message_id": "test-123"}
180
- return mock
181
-
182
-
183
- @pytest.fixture
184
- def mock_payment_gateway() -> MagicMock:
185
- """Mock payment gateway."""
186
- mock = MagicMock()
187
- mock.charge.return_value = {
188
- "id": "ch_123",
189
- "status": "succeeded",
190
- "amount": 1000,
191
- }
192
- mock.refund.return_value = {
193
- "id": "re_123",
194
- "status": "succeeded",
195
- }
196
- return mock
197
-
198
-
199
- # Async fixtures
200
- @pytest.fixture
201
- async def async_client() -> AsyncGenerator[AsyncClient, None]:
202
- """Create async HTTP client for API testing."""
203
- async with AsyncClient(app=app, base_url="http://test") as client:
204
- yield client
205
-
206
-
207
- @pytest.fixture
208
- def mock_async_service() -> AsyncMock:
209
- """Mock async service."""
210
- mock = AsyncMock()
211
- mock.fetch_data.return_value = {"data": "test"}
212
- return mock
213
-
214
-
215
- # Environment fixtures
216
- @pytest.fixture(autouse=True)
217
- def env_setup(monkeypatch):
218
- """Set up test environment variables."""
219
- monkeypatch.setenv("ENV", "test")
220
- monkeypatch.setenv("DEBUG", "false")
221
- monkeypatch.setenv("DATABASE_URL", "postgresql://test:test@localhost/test")
222
-
41
+ | Feature | Description | Reference |
42
+ |---------|-------------|-----------|
43
+ | Fixtures | Reusable test setup with dependency injection | [Fixtures Guide](https://docs.pytest.org/en/latest/how-to/fixtures.html) |
44
+ | Parametrization | Test multiple inputs with @pytest.mark.parametrize | [Parametrize](https://docs.pytest.org/en/latest/how-to/parametrize.html) |
45
+ | Mocking | unittest.mock integration for patching | [pytest-mock](https://pytest-mock.readthedocs.io/) |
46
+ | Async Testing | Native async/await support with pytest-asyncio | [pytest-asyncio](https://pytest-asyncio.readthedocs.io/) |
47
+ | Markers | Categorize and filter tests | [Markers](https://docs.pytest.org/en/latest/how-to/mark.html) |
48
+ | Coverage | Code coverage with pytest-cov | [pytest-cov](https://pytest-cov.readthedocs.io/) |
223
49
 
224
- # Cleanup fixtures
225
- @pytest.fixture(autouse=True)
226
- def cleanup_uploads(tmp_path):
227
- """Clean up uploaded files after tests."""
228
- yield
229
- # Cleanup happens automatically with tmp_path
230
- ```
50
+ ## Common Patterns
231
51
 
232
- ### 3. Parametrized Tests
52
+ ### Parametrized Validation Tests
233
53
 
234
54
  ```python
235
- # tests/test_validators.py
236
55
  import pytest
237
- from src.validators import (
238
- validate_email,
239
- validate_password,
240
- validate_phone,
241
- validate_url,
242
- ValidationError,
243
- )
244
-
56
+ from src.validators import validate_email, ValidationError
245
57
 
246
58
  class TestEmailValidation:
247
- """Test email validation."""
248
-
249
- @pytest.mark.parametrize(
250
- "email",
251
- [
252
- "user@example.com",
253
- "user.name@example.com",
254
- "user+tag@example.com",
255
- "user@subdomain.example.com",
256
- "user123@example.co.uk",
257
- ],
258
- )
59
+ @pytest.mark.parametrize("email", [
60
+ "user@example.com",
61
+ "user.name@example.com",
62
+ "user+tag@example.co.uk",
63
+ ])
259
64
  def test_valid_emails(self, email: str):
260
- """Test that valid emails pass validation."""
261
65
  assert validate_email(email) is True
262
66
 
263
- @pytest.mark.parametrize(
264
- "email,error_message",
265
- [
266
- ("", "Email is required"),
267
- ("invalid", "Invalid email format"),
268
- ("@example.com", "Invalid email format"),
269
- ("user@", "Invalid email format"),
270
- ("user@.com", "Invalid email format"),
271
- ("user space@example.com", "Invalid email format"),
272
- ],
273
- )
274
- def test_invalid_emails(self, email: str, error_message: str):
275
- """Test that invalid emails raise appropriate errors."""
276
- with pytest.raises(ValidationError) as exc_info:
67
+ @pytest.mark.parametrize("email,error", [
68
+ ("", "Email is required"),
69
+ ("invalid", "Invalid email format"),
70
+ ("@example.com", "Invalid email format"),
71
+ ])
72
+ def test_invalid_emails(self, email: str, error: str):
73
+ with pytest.raises(ValidationError, match=error):
277
74
  validate_email(email)
278
- assert error_message in str(exc_info.value)
279
-
280
-
281
- class TestPasswordValidation:
282
- """Test password validation."""
283
-
284
- @pytest.mark.parametrize(
285
- "password",
286
- [
287
- "SecurePass123!",
288
- "MyP@ssw0rd",
289
- "Complex!Pass99",
290
- "Valid#Password1",
291
- ],
292
- )
293
- def test_valid_passwords(self, password: str):
294
- """Test that valid passwords pass validation."""
295
- assert validate_password(password) is True
296
-
297
- @pytest.mark.parametrize(
298
- "password,expected_errors",
299
- [
300
- ("short", ["at least 8 characters"]),
301
- ("nouppercase123!", ["uppercase letter"]),
302
- ("NOLOWERCASE123!", ["lowercase letter"]),
303
- ("NoNumbers!", ["digit"]),
304
- ("NoSpecialChar123", ["special character"]),
305
- ("", ["Password is required"]),
306
- ],
307
- ids=[
308
- "too_short",
309
- "no_uppercase",
310
- "no_lowercase",
311
- "no_numbers",
312
- "no_special",
313
- "empty",
314
- ],
315
- )
316
- def test_invalid_passwords(self, password: str, expected_errors: list[str]):
317
- """Test that invalid passwords return appropriate errors."""
318
- with pytest.raises(ValidationError) as exc_info:
319
- validate_password(password)
320
-
321
- error_message = str(exc_info.value)
322
- for expected in expected_errors:
323
- assert expected in error_message
324
-
325
-
326
- class TestComplexParametrization:
327
- """Test complex parametrization patterns."""
328
-
329
- @pytest.mark.parametrize(
330
- "input_value,transform,expected",
331
- [
332
- (10, "double", 20),
333
- (10, "triple", 30),
334
- (5, "double", 10),
335
- (5, "triple", 15),
336
- ],
337
- )
338
- def test_transformations(self, input_value, transform, expected):
339
- """Test value transformations."""
340
- from src.transforms import apply_transform
75
+ ```
341
76
 
342
- result = apply_transform(input_value, transform)
343
- assert result == expected
77
+ ### Factory Fixtures
344
78
 
345
- @pytest.mark.parametrize("x", [1, 2, 3])
346
- @pytest.mark.parametrize("y", [10, 20])
347
- def test_cartesian_product(self, x: int, y: int):
348
- """Test all combinations of x and y."""
349
- from src.math_utils import multiply
79
+ ```python
80
+ @pytest.fixture
81
+ def user_factory(db_session):
82
+ """Factory for creating test users."""
83
+ def _create_user(email="test@example.com", name="Test User", **kwargs):
84
+ user = User(email=email, name=name, **kwargs)
85
+ db_session.add(user)
86
+ db_session.commit()
87
+ return user
88
+ return _create_user
350
89
 
351
- result = multiply(x, y)
352
- assert result == x * y
90
+ @pytest.fixture
91
+ def test_user(user_factory):
92
+ return user_factory(email="testuser@example.com")
353
93
  ```
354
94
 
355
- ### 4. Mocking and Patching
95
+ ### Mocking External Services
356
96
 
357
97
  ```python
358
- # tests/test_services.py
359
- import pytest
360
- from unittest.mock import patch, MagicMock, AsyncMock, call
361
- from datetime import datetime, timedelta
362
-
363
- from src.services.user_service import UserService
364
- from src.services.notification_service import NotificationService
365
- from src.services.payment_service import PaymentService
366
-
98
+ from unittest.mock import patch, MagicMock
367
99
 
368
100
  class TestUserService:
369
- """Test UserService with mocking."""
370
-
371
- @pytest.fixture
372
- def user_service(self, db_session, mock_email_service):
373
- """Create UserService with mocked dependencies."""
374
- return UserService(
375
- db=db_session,
376
- email_service=mock_email_service,
377
- )
378
-
379
- def test_create_user_sends_welcome_email(
380
- self,
381
- user_service: UserService,
382
- mock_email_service: MagicMock,
383
- ):
384
- """Test that creating a user sends welcome email."""
385
- user = user_service.create_user(
386
- email="new@example.com",
387
- name="New User",
388
- password="SecurePass123!",
389
- )
101
+ def test_create_user_sends_email(self, user_service, mock_email_service):
102
+ user = user_service.create_user(email="new@example.com", name="New")
390
103
 
391
104
  mock_email_service.send_email.assert_called_once_with(
392
105
  to="new@example.com",
393
106
  template="welcome",
394
- context={"name": "New User"},
107
+ context={"name": "New"},
395
108
  )
396
- assert user.email == "new@example.com"
397
109
 
398
110
  @patch("src.services.user_service.datetime")
399
- def test_user_last_login_updated(
400
- self,
401
- mock_datetime: MagicMock,
402
- user_service: UserService,
403
- test_user,
404
- ):
405
- """Test that last login is updated on authentication."""
406
- fixed_time = datetime(2024, 1, 15, 10, 30, 0)
407
- mock_datetime.utcnow.return_value = fixed_time
111
+ def test_last_login_updated(self, mock_datetime, user_service, test_user):
112
+ from datetime import datetime
113
+ mock_datetime.utcnow.return_value = datetime(2024, 1, 15, 10, 30)
408
114
 
409
115
  user_service.authenticate(test_user.email, "password")
410
-
411
- assert test_user.last_login == fixed_time
412
-
413
- def test_password_reset_flow(
414
- self,
415
- user_service: UserService,
416
- mock_email_service: MagicMock,
417
- test_user,
418
- ):
419
- """Test complete password reset flow."""
420
- # Request reset
421
- token = user_service.request_password_reset(test_user.email)
422
-
423
- mock_email_service.send_email.assert_called_with(
424
- to=test_user.email,
425
- template="password_reset",
426
- context={"token": token, "name": test_user.name},
427
- )
428
-
429
- # Reset password
430
- user_service.reset_password(token, "NewSecurePass123!")
431
-
432
- # Verify new password works
433
- assert user_service.authenticate(
434
- test_user.email,
435
- "NewSecurePass123!",
436
- )
437
-
438
-
439
- class TestNotificationService:
440
- """Test NotificationService with multiple mocks."""
441
-
442
- @pytest.fixture
443
- def notification_service(self):
444
- """Create NotificationService with mocked providers."""
445
- with patch("src.services.notification_service.SMSProvider") as mock_sms, \
446
- patch("src.services.notification_service.PushProvider") as mock_push:
447
-
448
- mock_sms_instance = MagicMock()
449
- mock_push_instance = MagicMock()
450
- mock_sms.return_value = mock_sms_instance
451
- mock_push.return_value = mock_push_instance
452
-
453
- service = NotificationService()
454
- service._sms = mock_sms_instance
455
- service._push = mock_push_instance
456
-
457
- yield service
458
-
459
- def test_send_notification_all_channels(
460
- self,
461
- notification_service: NotificationService,
462
- ):
463
- """Test sending notifications to all channels."""
464
- notification_service.notify(
465
- user_id="user_123",
466
- message="Test notification",
467
- channels=["sms", "push"],
468
- )
469
-
470
- notification_service._sms.send.assert_called_once()
471
- notification_service._push.send.assert_called_once()
472
-
473
- def test_notification_retry_on_failure(
474
- self,
475
- notification_service: NotificationService,
476
- ):
477
- """Test notification retry logic."""
478
- notification_service._sms.send.side_effect = [
479
- Exception("Network error"),
480
- Exception("Network error"),
481
- {"message_id": "success"},
482
- ]
483
-
484
- result = notification_service.notify(
485
- user_id="user_123",
486
- message="Test",
487
- channels=["sms"],
488
- max_retries=3,
489
- )
490
-
491
- assert notification_service._sms.send.call_count == 3
492
- assert result["sms"]["status"] == "success"
493
-
494
-
495
- class TestPaymentService:
496
- """Test PaymentService with payment gateway mock."""
497
-
498
- @pytest.fixture
499
- def payment_service(self, db_session, mock_payment_gateway):
500
- """Create PaymentService with mocked gateway."""
501
- return PaymentService(
502
- db=db_session,
503
- gateway=mock_payment_gateway,
504
- )
505
-
506
- def test_process_payment_success(
507
- self,
508
- payment_service: PaymentService,
509
- mock_payment_gateway: MagicMock,
510
- test_user,
511
- ):
512
- """Test successful payment processing."""
513
- result = payment_service.process_payment(
514
- user_id=test_user.id,
515
- amount=1000,
516
- currency="usd",
517
- payment_method_id="pm_123",
518
- )
519
-
520
- mock_payment_gateway.charge.assert_called_once_with(
521
- amount=1000,
522
- currency="usd",
523
- payment_method="pm_123",
524
- metadata={"user_id": test_user.id},
525
- )
526
- assert result["status"] == "succeeded"
527
-
528
- def test_process_payment_failure_rolls_back(
529
- self,
530
- payment_service: PaymentService,
531
- mock_payment_gateway: MagicMock,
532
- test_user,
533
- db_session,
534
- ):
535
- """Test that failed payment rolls back database changes."""
536
- mock_payment_gateway.charge.side_effect = Exception("Card declined")
537
-
538
- with pytest.raises(Exception, match="Card declined"):
539
- payment_service.process_payment(
540
- user_id=test_user.id,
541
- amount=1000,
542
- currency="usd",
543
- payment_method_id="pm_123",
544
- )
545
-
546
- # Verify no payment record was created
547
- from src.models import Payment
548
- payments = db_session.query(Payment).filter_by(user_id=test_user.id).all()
549
- assert len(payments) == 0
116
+ assert test_user.last_login == datetime(2024, 1, 15, 10, 30)
550
117
  ```
551
118
 
552
- ### 5. Async Testing
119
+ ### Async API Testing
553
120
 
554
121
  ```python
555
- # tests/test_async_services.py
556
122
  import pytest
557
- from unittest.mock import AsyncMock, patch
558
123
  from httpx import AsyncClient
559
124
 
560
- from src.services.async_service import AsyncDataService
561
- from src.main import app
562
-
563
-
564
- @pytest.mark.asyncio
565
- class TestAsyncDataService:
566
- """Test async data service."""
567
-
568
- @pytest.fixture
569
- def async_service(self) -> AsyncDataService:
570
- """Create async data service."""
571
- return AsyncDataService()
572
-
573
- async def test_fetch_data_success(
574
- self,
575
- async_service: AsyncDataService,
576
- ):
577
- """Test successful data fetching."""
578
- with patch.object(
579
- async_service,
580
- "_http_client",
581
- new_callable=AsyncMock,
582
- ) as mock_client:
583
- mock_client.get.return_value.json.return_value = {
584
- "items": [{"id": 1}, {"id": 2}]
585
- }
586
- mock_client.get.return_value.status_code = 200
587
-
588
- result = await async_service.fetch_data("test-endpoint")
589
-
590
- assert len(result["items"]) == 2
591
-
592
- async def test_fetch_data_with_retry(
593
- self,
594
- async_service: AsyncDataService,
595
- ):
596
- """Test data fetching with retry on failure."""
597
- with patch.object(
598
- async_service,
599
- "_http_client",
600
- new_callable=AsyncMock,
601
- ) as mock_client:
602
- # First two calls fail, third succeeds
603
- mock_client.get.side_effect = [
604
- Exception("Connection error"),
605
- Exception("Timeout"),
606
- AsyncMock(
607
- json=AsyncMock(return_value={"data": "success"}),
608
- status_code=200,
609
- ),
610
- ]
611
-
612
- result = await async_service.fetch_data_with_retry(
613
- "test-endpoint",
614
- max_retries=3,
615
- )
616
-
617
- assert result == {"data": "success"}
618
- assert mock_client.get.call_count == 3
619
-
620
- async def test_concurrent_fetches(
621
- self,
622
- async_service: AsyncDataService,
623
- ):
624
- """Test concurrent data fetching."""
625
- with patch.object(
626
- async_service,
627
- "_http_client",
628
- new_callable=AsyncMock,
629
- ) as mock_client:
630
- mock_client.get.return_value.json.return_value = {"id": 1}
631
- mock_client.get.return_value.status_code = 200
632
-
633
- results = await async_service.fetch_multiple(
634
- ["endpoint1", "endpoint2", "endpoint3"]
635
- )
636
-
637
- assert len(results) == 3
638
- assert mock_client.get.call_count == 3
639
-
640
-
641
125
  @pytest.mark.asyncio
642
126
  class TestAPIEndpoints:
643
- """Test API endpoints with async client."""
644
-
645
127
  async def test_get_users(self, async_client: AsyncClient, test_user):
646
- """Test GET /users endpoint."""
647
128
  response = await async_client.get("/api/users")
648
-
649
129
  assert response.status_code == 200
650
- data = response.json()
651
- assert len(data["users"]) >= 1
130
+ assert len(response.json()["users"]) >= 1
652
131
 
653
132
  async def test_create_user(self, async_client: AsyncClient):
654
- """Test POST /users endpoint."""
655
- response = await async_client.post(
656
- "/api/users",
657
- json={
658
- "email": "newuser@example.com",
659
- "name": "New User",
660
- "password": "SecurePass123!",
661
- },
662
- )
663
-
133
+ response = await async_client.post("/api/users", json={
134
+ "email": "new@example.com",
135
+ "name": "New User",
136
+ "password": "SecurePass123!",
137
+ })
664
138
  assert response.status_code == 201
665
- data = response.json()
666
- assert data["user"]["email"] == "newuser@example.com"
667
-
668
- async def test_authentication_flow(self, async_client: AsyncClient, test_user):
669
- """Test complete authentication flow."""
670
- # Login
671
- login_response = await async_client.post(
672
- "/api/auth/login",
673
- json={
674
- "email": test_user.email,
675
- "password": "testpassword",
676
- },
677
- )
678
- assert login_response.status_code == 200
679
- token = login_response.json()["access_token"]
680
-
681
- # Access protected endpoint
682
- protected_response = await async_client.get(
683
- "/api/me",
684
- headers={"Authorization": f"Bearer {token}"},
685
- )
686
- assert protected_response.status_code == 200
687
- assert protected_response.json()["email"] == test_user.email
688
- ```
689
-
690
- ### 6. Test Markers and Custom Plugins
691
-
692
- ```python
693
- # tests/markers.py
694
- import pytest
695
- from functools import wraps
696
-
697
-
698
- def slow(func):
699
- """Mark test as slow."""
700
- return pytest.mark.slow(func)
701
-
702
-
703
- def integration(func):
704
- """Mark test as integration test."""
705
- return pytest.mark.integration(func)
706
-
707
-
708
- def requires_db(func):
709
- """Mark test as requiring database."""
710
- return pytest.mark.requires_db(func)
711
-
712
-
713
- # tests/conftest.py - Custom markers configuration
714
- def pytest_configure(config):
715
- """Configure custom markers."""
716
- config.addinivalue_line(
717
- "markers",
718
- "slow: marks tests as slow (deselect with '-m \"not slow\"')",
719
- )
720
- config.addinivalue_line(
721
- "markers",
722
- "integration: marks tests as integration tests",
723
- )
724
- config.addinivalue_line(
725
- "markers",
726
- "requires_db: marks tests as requiring database connection",
727
- )
728
-
729
-
730
- def pytest_collection_modifyitems(config, items):
731
- """Modify test collection based on markers."""
732
- if config.getoption("--skip-slow"):
733
- skip_slow = pytest.mark.skip(reason="--skip-slow option provided")
734
- for item in items:
735
- if "slow" in item.keywords:
736
- item.add_marker(skip_slow)
737
-
738
- if not config.getoption("--run-integration"):
739
- skip_integration = pytest.mark.skip(
740
- reason="need --run-integration option to run"
741
- )
742
- for item in items:
743
- if "integration" in item.keywords:
744
- item.add_marker(skip_integration)
745
-
746
-
747
- def pytest_addoption(parser):
748
- """Add custom command line options."""
749
- parser.addoption(
750
- "--skip-slow",
751
- action="store_true",
752
- default=False,
753
- help="Skip slow tests",
754
- )
755
- parser.addoption(
756
- "--run-integration",
757
- action="store_true",
758
- default=False,
759
- help="Run integration tests",
760
- )
761
-
762
-
763
- # tests/test_with_markers.py
764
- import pytest
765
-
766
-
767
- @pytest.mark.slow
768
- def test_slow_operation():
769
- """This test takes a long time."""
770
- import time
771
- time.sleep(5)
772
- assert True
773
-
774
-
775
- @pytest.mark.integration
776
- def test_external_api():
777
- """This test requires external API."""
778
- import requests
779
- response = requests.get("https://api.example.com/health")
780
- assert response.status_code == 200
781
-
782
-
783
- @pytest.mark.requires_db
784
- def test_database_operation(db_session):
785
- """This test requires database."""
786
- result = db_session.execute("SELECT 1")
787
- assert result.scalar() == 1
788
- ```
789
-
790
- ## Use Cases
791
-
792
- ### Test Organization for Large Projects
793
-
794
- ```
795
- tests/
796
- ├── conftest.py # Shared fixtures
797
- ├── unit/ # Unit tests
798
- │ ├── conftest.py
799
- │ ├── test_models.py
800
- │ ├── test_validators.py
801
- │ └── test_utils.py
802
- ├── integration/ # Integration tests
803
- │ ├── conftest.py
804
- │ ├── test_api.py
805
- │ ├── test_database.py
806
- │ └── test_external_services.py
807
- ├── e2e/ # End-to-end tests
808
- │ ├── conftest.py
809
- │ └── test_workflows.py
810
- └── fixtures/ # Shared test data
811
- ├── users.json
812
- └── products.json
813
- ```
814
-
815
- ### CI/CD Integration
816
-
817
- ```yaml
818
- # .github/workflows/test.yml
819
- name: Tests
820
-
821
- on: [push, pull_request]
822
-
823
- jobs:
824
- test:
825
- runs-on: ubuntu-latest
826
- services:
827
- postgres:
828
- image: postgres:15
829
- env:
830
- POSTGRES_PASSWORD: test
831
- POSTGRES_DB: test_db
832
- ports:
833
- - 5432:5432
834
-
835
- steps:
836
- - uses: actions/checkout@v4
837
-
838
- - name: Set up Python
839
- uses: actions/setup-python@v5
840
- with:
841
- python-version: "3.12"
842
-
843
- - name: Install dependencies
844
- run: |
845
- pip install -r requirements-dev.txt
846
-
847
- - name: Run unit tests
848
- run: pytest tests/unit -v --cov=src --cov-report=xml
849
-
850
- - name: Run integration tests
851
- run: pytest tests/integration -v --run-integration
852
- env:
853
- DATABASE_URL: postgresql://postgres:test@localhost:5432/test_db
854
-
855
- - name: Upload coverage
856
- uses: codecov/codecov-action@v4
857
- with:
858
- file: coverage.xml
859
139
  ```
860
140
 
861
141
  ## Best Practices
862
142
 
863
- ### Do's
864
-
865
- - Use descriptive test names that explain the scenario
866
- - Create reusable fixtures for common setup
867
- - Use parametrize for testing multiple inputs
868
- - Mock external dependencies
869
- - Group related tests in classes
870
- - Use markers to categorize tests
871
- - Write tests before or alongside code
872
- - Keep tests independent and isolated
873
- - Use factories for test data creation
874
- - Run tests in parallel with pytest-xdist
875
-
876
- ### Don'ts
877
-
878
- - Don't share state between tests
879
- - Don't use sleep for timing issues
880
- - Don't test implementation details
881
- - Don't write tests that depend on order
882
- - Don't ignore flaky tests
883
- - Don't over-mock (test real behavior when possible)
884
- - Don't use hardcoded paths
885
- - Don't skip writing tests for edge cases
886
- - Don't leave commented-out test code
887
- - Don't test framework functionality
143
+ | Do | Avoid |
144
+ |----|-------|
145
+ | Use descriptive test names explaining the scenario | Sharing state between tests |
146
+ | Create reusable fixtures for common setup | Using sleep for timing issues |
147
+ | Use parametrize for testing multiple inputs | Testing implementation details |
148
+ | Mock external dependencies | Writing tests that depend on order |
149
+ | Group related tests in classes | Using hardcoded file paths |
150
+ | Use factories for test data creation | Leaving commented-out test code |
888
151
 
889
152
  ## References
890
153
 
@@ -892,4 +155,3 @@ jobs:
892
155
  - [pytest-asyncio](https://pytest-asyncio.readthedocs.io/)
893
156
  - [pytest-mock](https://pytest-mock.readthedocs.io/)
894
157
  - [pytest-cov](https://pytest-cov.readthedocs.io/)
895
- - [Testing Best Practices](https://docs.pytest.org/en/latest/explanation/goodpractices.html)