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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/plugin/skills/databases/database-management/SKILL.md +288 -0
- package/plugin/skills/databases/database-migration/SKILL.md +285 -0
- package/plugin/skills/databases/database-schema-design/SKILL.md +195 -0
- package/plugin/skills/databases/mongodb/SKILL.md +60 -776
- package/plugin/skills/databases/prisma/SKILL.md +53 -744
- package/plugin/skills/databases/redis/SKILL.md +53 -860
- package/plugin/skills/databases/supabase/SKILL.md +283 -0
- package/plugin/skills/devops/aws/SKILL.md +68 -672
- package/plugin/skills/devops/github-actions/SKILL.md +54 -657
- package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +87 -853
- package/plugin/skills/frameworks/express/SKILL.md +95 -1301
- package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
- package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
- package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
- package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
- package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
- package/plugin/skills/frontend/responsive/SKILL.md +76 -799
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
- package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
- package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
- package/plugin/skills/languages/javascript/SKILL.md +106 -849
- package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
- package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
- package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
- package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
- package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
- package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
- package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
- package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
- package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
- package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
- package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
- package/plugin/skills/security/better-auth/SKILL.md +46 -1034
- package/plugin/skills/security/oauth/SKILL.md +80 -934
- package/plugin/skills/security/owasp/SKILL.md +78 -862
- package/plugin/skills/testing/playwright/SKILL.md +77 -700
- package/plugin/skills/testing/pytest/SKILL.md +73 -811
- package/plugin/skills/testing/vitest/SKILL.md +60 -920
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
- package/plugin/skills/SKILL_STANDARDS.md +0 -743
|
@@ -1,890 +1,153 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: Python
|
|
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
|
-
|
|
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
|
|
38
|
-
|
|
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
|
|
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
|
|
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) ->
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
""
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
"
|
|
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
|
-
"
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
343
|
-
assert result == expected
|
|
77
|
+
### Factory Fixtures
|
|
344
78
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
352
|
-
|
|
90
|
+
@pytest.fixture
|
|
91
|
+
def test_user(user_factory):
|
|
92
|
+
return user_factory(email="testuser@example.com")
|
|
353
93
|
```
|
|
354
94
|
|
|
355
|
-
###
|
|
95
|
+
### Mocking External Services
|
|
356
96
|
|
|
357
97
|
```python
|
|
358
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
400
|
-
|
|
401
|
-
mock_datetime
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
"
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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)
|