omgkit 2.1.1 → 2.2.0

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 (50) hide show
  1. package/package.json +1 -1
  2. package/plugin/skills/SKILL_STANDARDS.md +743 -0
  3. package/plugin/skills/databases/mongodb/SKILL.md +797 -28
  4. package/plugin/skills/databases/prisma/SKILL.md +776 -30
  5. package/plugin/skills/databases/redis/SKILL.md +885 -25
  6. package/plugin/skills/devops/aws/SKILL.md +686 -28
  7. package/plugin/skills/devops/github-actions/SKILL.md +684 -29
  8. package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
  9. package/plugin/skills/frameworks/django/SKILL.md +920 -20
  10. package/plugin/skills/frameworks/express/SKILL.md +1361 -35
  11. package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
  12. package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
  13. package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
  14. package/plugin/skills/frameworks/rails/SKILL.md +594 -28
  15. package/plugin/skills/frameworks/spring/SKILL.md +528 -35
  16. package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
  17. package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
  18. package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
  19. package/plugin/skills/frontend/responsive/SKILL.md +847 -21
  20. package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
  21. package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
  22. package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
  23. package/plugin/skills/languages/javascript/SKILL.md +935 -31
  24. package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
  25. package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
  26. package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
  27. package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
  28. package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
  29. package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
  30. package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
  31. package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
  32. package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
  33. package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
  34. package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
  35. package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
  36. package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
  37. package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
  38. package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
  39. package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
  40. package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
  41. package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
  42. package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
  43. package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
  44. package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
  45. package/plugin/skills/security/better-auth/SKILL.md +1065 -28
  46. package/plugin/skills/security/oauth/SKILL.md +968 -31
  47. package/plugin/skills/security/owasp/SKILL.md +894 -33
  48. package/plugin/skills/testing/playwright/SKILL.md +764 -38
  49. package/plugin/skills/testing/pytest/SKILL.md +873 -36
  50. package/plugin/skills/testing/vitest/SKILL.md +980 -35
@@ -1,58 +1,895 @@
1
1
  ---
2
2
  name: pytest
3
- description: Python testing with pytest. Use for unit tests, fixtures, mocking.
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
4
11
  ---
5
12
 
6
- # Pytest Skill
13
+ # Pytest
7
14
 
8
- ## Basic Tests
9
- ```python
10
- def test_add():
11
- assert add(1, 2) == 3
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
12
32
 
13
- def test_raises():
14
- with pytest.raises(ValueError):
15
- validate("")
33
+ ```ini
34
+ # pytest.ini
35
+ [pytest]
36
+ 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
49
+ markers =
50
+ slow: marks tests as slow (deselect with '-m "not slow"')
51
+ 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
+ asyncio_mode = auto
16
58
  ```
17
59
 
18
- ## Fixtures
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
+
19
89
  ```python
90
+ # tests/conftest.py
91
+ from typing import Generator, AsyncGenerator
92
+ from collections.abc import Iterator
93
+ 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
+
116
+
20
117
  @pytest.fixture
21
- def user():
22
- return User(email="test@example.com")
118
+ def db_session(engine) -> Generator[Session, None, None]:
119
+ """Create database session with automatic rollback."""
120
+ connection = engine.connect()
121
+ transaction = connection.begin()
122
+ session = sessionmaker(bind=connection)()
23
123
 
124
+ yield session
125
+
126
+ session.close()
127
+ transaction.rollback()
128
+ connection.close()
129
+
130
+
131
+ # Factory fixtures
24
132
  @pytest.fixture
25
- async def db():
26
- conn = await create_connection()
27
- yield conn
28
- await conn.close()
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
+
29
223
 
30
- def test_user_email(user):
31
- assert user.email == "test@example.com"
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
32
230
  ```
33
231
 
34
- ## Parametrize
232
+ ### 3. Parametrized Tests
233
+
35
234
  ```python
36
- @pytest.mark.parametrize("input,expected", [
37
- (1, 2),
38
- (2, 4),
39
- (3, 6),
40
- ])
41
- def test_double(input, expected):
42
- assert double(input) == expected
235
+ # tests/test_validators.py
236
+ import pytest
237
+ from src.validators import (
238
+ validate_email,
239
+ validate_password,
240
+ validate_phone,
241
+ validate_url,
242
+ ValidationError,
243
+ )
244
+
245
+
246
+ 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
+ )
259
+ def test_valid_emails(self, email: str):
260
+ """Test that valid emails pass validation."""
261
+ assert validate_email(email) is True
262
+
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:
277
+ 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
341
+
342
+ result = apply_transform(input_value, transform)
343
+ assert result == expected
344
+
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
350
+
351
+ result = multiply(x, y)
352
+ assert result == x * y
43
353
  ```
44
354
 
45
- ## Mocking
355
+ ### 4. Mocking and Patching
356
+
46
357
  ```python
47
- def test_api_call(mocker):
48
- mocker.patch('module.fetch', return_value={'data': 'test'})
49
- result = get_data()
50
- assert result == {'data': 'test'}
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
+
367
+
368
+ 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
+ )
390
+
391
+ mock_email_service.send_email.assert_called_once_with(
392
+ to="new@example.com",
393
+ template="welcome",
394
+ context={"name": "New User"},
395
+ )
396
+ assert user.email == "new@example.com"
397
+
398
+ @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
408
+
409
+ 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
550
+ ```
551
+
552
+ ### 5. Async Testing
553
+
554
+ ```python
555
+ # tests/test_async_services.py
556
+ import pytest
557
+ from unittest.mock import AsyncMock, patch
558
+ from httpx import AsyncClient
559
+
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
+ @pytest.mark.asyncio
642
+ class TestAPIEndpoints:
643
+ """Test API endpoints with async client."""
644
+
645
+ async def test_get_users(self, async_client: AsyncClient, test_user):
646
+ """Test GET /users endpoint."""
647
+ response = await async_client.get("/api/users")
648
+
649
+ assert response.status_code == 200
650
+ data = response.json()
651
+ assert len(data["users"]) >= 1
652
+
653
+ 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
+
664
+ 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
+
51
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"
52
842
 
53
- ## Run
54
- ```bash
55
- pytest -v
56
- pytest --cov=src
57
- pytest -k "test_user"
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
58
859
  ```
860
+
861
+ ## Best Practices
862
+
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
888
+
889
+ ## References
890
+
891
+ - [Pytest Documentation](https://docs.pytest.org/)
892
+ - [pytest-asyncio](https://pytest-asyncio.readthedocs.io/)
893
+ - [pytest-mock](https://pytest-mock.readthedocs.io/)
894
+ - [pytest-cov](https://pytest-cov.readthedocs.io/)
895
+ - [Testing Best Practices](https://docs.pytest.org/en/latest/explanation/goodpractices.html)