python-skills 1.0.0__py3-none-any.whl
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.
- python_skills/__init__.py +10 -0
- python_skills/__main__.py +6 -0
- python_skills/adapters/__init__.py +48 -0
- python_skills/adapters/agent_skills.py +415 -0
- python_skills/adapters/aider_adapter.py +226 -0
- python_skills/adapters/base.py +153 -0
- python_skills/adapters/claude.py +474 -0
- python_skills/adapters/cline.py +332 -0
- python_skills/adapters/codex.py +24 -0
- python_skills/adapters/continue_adapter.py +198 -0
- python_skills/adapters/cursor.py +327 -0
- python_skills/adapters/gemini.py +26 -0
- python_skills/adapters/goose.py +26 -0
- python_skills/adapters/junie.py +25 -0
- python_skills/adapters/kiro.py +382 -0
- python_skills/adapters/opencode.py +27 -0
- python_skills/adapters/roo.py +25 -0
- python_skills/adapters/universal.py +203 -0
- python_skills/adapters/vscode.py +27 -0
- python_skills/adapters/windsurf.py +26 -0
- python_skills/adapters/zed.py +27 -0
- python_skills/cli.py +326 -0
- python_skills/config.py +160 -0
- python_skills/detector.py +152 -0
- python_skills/installer.py +163 -0
- python_skills/markers.py +115 -0
- python_skills/skills/__init__.py +14 -0
- python_skills/skills/loader.py +171 -0
- python_skills/skills/metadata.py +152 -0
- python_skills/skills/registry.py +101 -0
- python_skills/state.py +204 -0
- python_skills-1.0.0.dist-info/METADATA +99 -0
- python_skills-1.0.0.dist-info/RECORD +105 -0
- python_skills-1.0.0.dist-info/WHEEL +4 -0
- python_skills-1.0.0.dist-info/entry_points.txt +2 -0
- python_skills-1.0.0.dist-info/licenses/LICENSE +21 -0
- skills/advanced_python.md +239 -0
- skills/anti_patterns/index.md +406 -0
- skills/comprehensions.md +167 -0
- skills/control_flow.md +175 -0
- skills/data_structures.md +243 -0
- skills/debugging/common_bugs.md +222 -0
- skills/debugging/inspection_techniques.md +249 -0
- skills/debugging/root_cause.md +203 -0
- skills/engineering/application_logging.md +195 -0
- skills/engineering/cli_apps.md +207 -0
- skills/engineering/configuration.md +218 -0
- skills/engineering/database.md +240 -0
- skills/engineering/dependency_management.md +205 -0
- skills/engineering/http_clients.md +267 -0
- skills/engineering/modules_packages.md +211 -0
- skills/engineering/packaging.md +197 -0
- skills/engineering/project_structure.md +155 -0
- skills/engineering/pyproject_toml.md +302 -0
- skills/engineering/virtual_environments.md +206 -0
- skills/functions.md +244 -0
- skills/generation/async_concurrency.md +291 -0
- skills/generation/error_handling.md +276 -0
- skills/generation/protocols_generics.md +243 -0
- skills/generation/type_hints.md +290 -0
- skills/generation/validation_pipeline.md +274 -0
- skills/generation/workflow.md +190 -0
- skills/oop.md +228 -0
- skills/quality/abstractions.md +154 -0
- skills/quality/comments.md +177 -0
- skills/quality/documentation.md +176 -0
- skills/quality/duplication.md +137 -0
- skills/quality/maintainability.md +142 -0
- skills/quality/naming.md +171 -0
- skills/quality/quality_functions.md +245 -0
- skills/quality/readability.md +239 -0
- skills/quality/type_annotations.md +192 -0
- skills/refactoring/behavior_preservation.md +157 -0
- skills/refactoring/incremental.md +187 -0
- skills/refactoring/interface_stability.md +199 -0
- skills/refactoring/safe_refactoring.md +206 -0
- skills/security/auth_boundaries.md +200 -0
- skills/security/command_injection.md +207 -0
- skills/security/dependency_risks.md +282 -0
- skills/security/file_handling.md +156 -0
- skills/security/input_validation.md +190 -0
- skills/security/path_traversal.md +172 -0
- skills/security/secrets.md +171 -0
- skills/security/sql_injection.md +188 -0
- skills/security/unsafe_deserialization.md +164 -0
- skills/stdlib/argparse.md +178 -0
- skills/stdlib/collections.md +212 -0
- skills/stdlib/datetime.md +187 -0
- skills/stdlib/functools.md +238 -0
- skills/stdlib/itertools.md +183 -0
- skills/stdlib/json.md +162 -0
- skills/stdlib/logging.md +185 -0
- skills/stdlib/os_sys.md +184 -0
- skills/stdlib/pathlib.md +218 -0
- skills/stdlib/re.md +171 -0
- skills/stdlib/statistics.md +112 -0
- skills/stdlib/subprocess.md +211 -0
- skills/testing/async_tests.md +249 -0
- skills/testing/coverage.md +168 -0
- skills/testing/edge_cases.md +197 -0
- skills/testing/fixtures_mocks.md +203 -0
- skills/testing/organization.md +205 -0
- skills/testing/parameterized.md +174 -0
- skills/testing/regression_tests.md +165 -0
- skills/variables_types.md +107 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Testing: Fixtures and Mocks
|
|
2
|
+
|
|
3
|
+
**Purpose**: Effective test fixtures and mocking strategies.
|
|
4
|
+
|
|
5
|
+
**When to use**: Writing tests that need setup, dependencies, or isolation.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Core Rules
|
|
10
|
+
|
|
11
|
+
### Fixture Principles
|
|
12
|
+
- **Scope appropriately**: `function` (default), `class`, `module`, `session`
|
|
13
|
+
- **Single responsibility**: Each fixture does one thing
|
|
14
|
+
- **Explicit dependencies**: Fixtures request what they need
|
|
15
|
+
- **Cleanup**: Use `yield` for teardown
|
|
16
|
+
|
|
17
|
+
### Fixture Patterns
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
# conftest.py
|
|
21
|
+
|
|
22
|
+
# Session-scoped expensive resource
|
|
23
|
+
@pytest.fixture(scope="session")
|
|
24
|
+
def database():
|
|
25
|
+
db = create_test_database()
|
|
26
|
+
yield db
|
|
27
|
+
db.drop()
|
|
28
|
+
|
|
29
|
+
# Function-scoped clean state
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def clean_db(database):
|
|
32
|
+
database.truncate_all()
|
|
33
|
+
yield database
|
|
34
|
+
database.truncate_all()
|
|
35
|
+
|
|
36
|
+
# Factory fixture
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def user_factory(clean_db):
|
|
39
|
+
def _create(email: str = "test@test.com", **kwargs):
|
|
40
|
+
return UserService(clean_db).create(email, **kwargs)
|
|
41
|
+
return _create
|
|
42
|
+
|
|
43
|
+
# Parametrized fixture
|
|
44
|
+
@pytest.fixture(params=["sqlite", "postgresql"])
|
|
45
|
+
def db_engine(request):
|
|
46
|
+
if request.param == "sqlite":
|
|
47
|
+
return create_sqlite_engine()
|
|
48
|
+
return create_pg_engine()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Mocking Guidelines
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# Mock at the boundary (where your code calls external code)
|
|
55
|
+
# NOT deep in the call stack
|
|
56
|
+
|
|
57
|
+
# GOOD — mock the client your code uses
|
|
58
|
+
@patch("mypackage.services.payment.StripeClient")
|
|
59
|
+
def test_charge(mock_stripe, payment_service):
|
|
60
|
+
mock_stripe.return_value.charge.return_value = ChargeResult(success=True)
|
|
61
|
+
result = payment_service.charge(100, "token")
|
|
62
|
+
assert result.success
|
|
63
|
+
|
|
64
|
+
# BAD — mock internal implementation
|
|
65
|
+
@patch("mypackage.services.payment.calculate_fee")
|
|
66
|
+
def test_charge_bad(mock_fee, payment_service):
|
|
67
|
+
...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Mock Types
|
|
71
|
+
```python
|
|
72
|
+
from unittest.mock import Mock, MagicMock, AsyncMock, PropertyMock
|
|
73
|
+
|
|
74
|
+
# Sync mock
|
|
75
|
+
mock = Mock()
|
|
76
|
+
mock.method.return_value = "value"
|
|
77
|
+
mock.method.side_effect = Exception("error")
|
|
78
|
+
mock.property = PropertyMock(return_value="value")
|
|
79
|
+
|
|
80
|
+
# Async mock
|
|
81
|
+
async_mock = AsyncMock()
|
|
82
|
+
async_mock.async_method.return_value = "value"
|
|
83
|
+
async_mock.async_method.side_effect = [val1, val2, Exception("error")]
|
|
84
|
+
|
|
85
|
+
# Spec (prevents typos)
|
|
86
|
+
mock = Mock(spec=RealClass)
|
|
87
|
+
mock.real_method() # OK
|
|
88
|
+
mock.typo_method() # AttributeError!
|
|
89
|
+
|
|
90
|
+
# Autospec (signature checking)
|
|
91
|
+
mock = Mock(autospec=RealClass)
|
|
92
|
+
mock.real_method(1, 2) # OK
|
|
93
|
+
mock.real_method() # TypeError: missing args
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Test Data Builders
|
|
97
|
+
```python
|
|
98
|
+
# For complex test objects
|
|
99
|
+
class UserBuilder:
|
|
100
|
+
def __init__(self):
|
|
101
|
+
self._email = "test@test.com"
|
|
102
|
+
self._name = "Test User"
|
|
103
|
+
self._active = True
|
|
104
|
+
self._roles = []
|
|
105
|
+
|
|
106
|
+
def with_email(self, email: str) -> "UserBuilder":
|
|
107
|
+
self._email = email
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
def with_name(self, name: str) -> "UserBuilder":
|
|
111
|
+
self._name = name
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def inactive(self) -> "UserBuilder":
|
|
115
|
+
self._active = False
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def with_roles(self, *roles) -> "UserBuilder":
|
|
119
|
+
self._roles = list(roles)
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
def build(self) -> User:
|
|
123
|
+
return User(
|
|
124
|
+
email=self._email,
|
|
125
|
+
name=self._name,
|
|
126
|
+
active=self._active,
|
|
127
|
+
roles=self._roles,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Usage
|
|
131
|
+
def test_admin_user(user_factory):
|
|
132
|
+
admin = UserBuilder().with_roles("admin").build()
|
|
133
|
+
assert admin.has_permission("admin")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Decision Rules
|
|
139
|
+
|
|
140
|
+
| Need | Fixture/Mock Pattern |
|
|
141
|
+
|------|---------------------|
|
|
142
|
+
| Shared expensive resource | `scope="session"` fixture |
|
|
143
|
+
| Clean state per test | Function fixture with cleanup |
|
|
144
|
+
| Test data variations | Factory fixture or builder |
|
|
145
|
+
| External service | Mock at boundary |
|
|
146
|
+
| Time-dependent | `freezegun` / mock `time.time` |
|
|
147
|
+
| Random-dependent | Mock `random` / `secrets` |
|
|
148
|
+
| Multiple implementations | Parametrized fixture |
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Preferred Patterns
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# Conftest organization
|
|
156
|
+
# tests/conftest.py — project-wide
|
|
157
|
+
# tests/unit/conftest.py — unit-specific
|
|
158
|
+
# tests/integration/conftest.py — integration-specific
|
|
159
|
+
|
|
160
|
+
# Fixture naming
|
|
161
|
+
@pytest.fixture
|
|
162
|
+
def user_service() -> UserService: # Return type hint
|
|
163
|
+
...
|
|
164
|
+
|
|
165
|
+
# Async fixtures
|
|
166
|
+
@pytest.fixture
|
|
167
|
+
async def async_client() -> AsyncClient:
|
|
168
|
+
client = AsyncClient()
|
|
169
|
+
yield client
|
|
170
|
+
await client.aclose()
|
|
171
|
+
|
|
172
|
+
# Mock fixture
|
|
173
|
+
@pytest.fixture
|
|
174
|
+
def mock_email_service() -> Mock:
|
|
175
|
+
return Mock(spec=EmailService)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Avoid
|
|
181
|
+
|
|
182
|
+
- Fixtures with side effects (no cleanup)
|
|
183
|
+
- Over-mocking (mock everything, test nothing)
|
|
184
|
+
- Mocking standard library (mock `requests`, not `json`)
|
|
185
|
+
- Fixtures that depend on test order
|
|
186
|
+
- Complex fixture chains (>3 levels)
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Validation Considerations
|
|
191
|
+
|
|
192
|
+
- Fixture setup/teardown time (profile with `--durations=10`)
|
|
193
|
+
- Mock call verification (`assert_called_once_with`)
|
|
194
|
+
- No real network calls in unit tests (use `pytest-mock` + `requests-mock`)
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Related Skills
|
|
199
|
+
|
|
200
|
+
- `testing/organization.md`
|
|
201
|
+
- `testing/parameterized.md`
|
|
202
|
+
- `testing/async_tests.md`
|
|
203
|
+
- `generation/error_handling.md`
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Testing: Organization
|
|
2
|
+
|
|
3
|
+
**Purpose**: Structuring test suites for maintainability and speed.
|
|
4
|
+
|
|
5
|
+
**When to use**: Setting up or reorganizing test structure.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Core Rules
|
|
10
|
+
|
|
11
|
+
### Directory Structure
|
|
12
|
+
```
|
|
13
|
+
tests/
|
|
14
|
+
├── conftest.py # Root fixtures
|
|
15
|
+
├── pytest.ini # Config (or pyproject.toml)
|
|
16
|
+
├── unit/ # Fast, isolated
|
|
17
|
+
│ ├── conftest.py
|
|
18
|
+
│ ├── test_models.py
|
|
19
|
+
│ ├── test_services.py
|
|
20
|
+
│ └── test_utils.py
|
|
21
|
+
├── integration/ # Real dependencies
|
|
22
|
+
│ ├── conftest.py
|
|
23
|
+
│ ├── test_database.py
|
|
24
|
+
│ ├── test_api.py
|
|
25
|
+
│ └── test_external.py
|
|
26
|
+
├── e2e/ # Full stack (optional)
|
|
27
|
+
│ ├── conftest.py
|
|
28
|
+
│ └── test_flows.py
|
|
29
|
+
├── performance/ # Benchmarks (optional)
|
|
30
|
+
│ └── test_benchmarks.py
|
|
31
|
+
├── fixtures/ # Shared test data
|
|
32
|
+
│ ├── sample_users.json
|
|
33
|
+
│ └── sample_orders.json
|
|
34
|
+
└── helpers/ # Test utilities
|
|
35
|
+
├── __init__.py
|
|
36
|
+
├── builders.py
|
|
37
|
+
└── matchers.py
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Naming Conventions
|
|
41
|
+
```
|
|
42
|
+
test_<module>.py # Module tests
|
|
43
|
+
test_<class>.py # Class tests
|
|
44
|
+
test_<function>_<scenario>.py # Specific scenario
|
|
45
|
+
Test<ClassName> # Test class
|
|
46
|
+
test_<function>_<condition> # Test function
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Test Class Organization
|
|
50
|
+
```python
|
|
51
|
+
class TestUserService:
|
|
52
|
+
"""Tests for UserService."""
|
|
53
|
+
|
|
54
|
+
class TestCreateUser:
|
|
55
|
+
"""Tests for create_user method."""
|
|
56
|
+
|
|
57
|
+
def test_creates_user_with_valid_data(self):
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
def test_raises_on_duplicate_email(self):
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
def test_hashes_password(self):
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
class TestGetUser:
|
|
67
|
+
"""Tests for get_user method."""
|
|
68
|
+
|
|
69
|
+
def test_returns_user_when_exists(self):
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
def test_returns_none_when_not_found(self):
|
|
73
|
+
...
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Marks for Categorization
|
|
77
|
+
```python
|
|
78
|
+
# pytest.ini
|
|
79
|
+
[tool.pytest.ini_options]
|
|
80
|
+
markers = [
|
|
81
|
+
"unit: Fast, isolated tests",
|
|
82
|
+
"integration: Tests with real dependencies",
|
|
83
|
+
"e2e: Full system tests",
|
|
84
|
+
"slow: Takes >1s",
|
|
85
|
+
"requires_db: Needs database",
|
|
86
|
+
"requires_network: Needs external API",
|
|
87
|
+
"regression: Bug fix verification",
|
|
88
|
+
"security: Security-related tests",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
# Usage
|
|
92
|
+
@pytest.mark.unit
|
|
93
|
+
def test_unit():
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
@pytest.mark.integration
|
|
97
|
+
@pytest.mark.requires_db
|
|
98
|
+
def test_with_db():
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
# Run subsets
|
|
102
|
+
# pytest -m unit
|
|
103
|
+
# pytest -m "integration and not slow"
|
|
104
|
+
# pytest -m "not e2e"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Configuration
|
|
108
|
+
```toml
|
|
109
|
+
# pyproject.toml
|
|
110
|
+
[tool.pytest.ini_options]
|
|
111
|
+
testpaths = ["tests"]
|
|
112
|
+
python_files = ["test_*.py"]
|
|
113
|
+
python_classes = ["Test*"]
|
|
114
|
+
python_functions = ["test_*"]
|
|
115
|
+
addopts = "-v --strict-markers --strict-config --tb=short"
|
|
116
|
+
filterwarnings = [
|
|
117
|
+
"ignore::DeprecationWarning",
|
|
118
|
+
"ignore::PendingDeprecationWarning",
|
|
119
|
+
]
|
|
120
|
+
asyncio_mode = "auto"
|
|
121
|
+
|
|
122
|
+
[tool.coverage.run]
|
|
123
|
+
source = ["src"]
|
|
124
|
+
omit = ["tests/*", "*/__main__.py"]
|
|
125
|
+
|
|
126
|
+
[tool.coverage.report]
|
|
127
|
+
exclude_lines = [
|
|
128
|
+
"pragma: no cover",
|
|
129
|
+
"def __repr__",
|
|
130
|
+
"raise AssertionError",
|
|
131
|
+
"raise NotImplementedError",
|
|
132
|
+
"if __name__ == .__main__.:",
|
|
133
|
+
]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Decision Rules
|
|
139
|
+
|
|
140
|
+
| Test Type | Location | Markers | Speed Target |
|
|
141
|
+
|-----------|----------|---------|--------------|
|
|
142
|
+
| Unit | `tests/unit/` | `unit` | <100ms |
|
|
143
|
+
| Integration | `tests/integration/` | `integration`, `requires_db` | <1s |
|
|
144
|
+
| E2E | `tests/e2e/` | `e2e` | <30s |
|
|
145
|
+
| Regression | `tests/regression/` | `regression` | Varies |
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Preferred Patterns
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
# Shared test utilities
|
|
153
|
+
# tests/helpers/builders.py
|
|
154
|
+
class UserBuilder:
|
|
155
|
+
def __init__(self):
|
|
156
|
+
self.data = {"email": "test@test.com", "name": "Test"}
|
|
157
|
+
|
|
158
|
+
def with_email(self, email):
|
|
159
|
+
self.data["email"] = email
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
def build(self):
|
|
163
|
+
return User(**self.data)
|
|
164
|
+
|
|
165
|
+
# tests/helpers/matchers.py
|
|
166
|
+
def assert_user_equal(actual: User, expected: User):
|
|
167
|
+
assert actual.id == expected.id
|
|
168
|
+
assert actual.email == expected.email
|
|
169
|
+
assert actual.name == expected.name
|
|
170
|
+
# Don't compare timestamps (flaky)
|
|
171
|
+
|
|
172
|
+
# Custom assertions
|
|
173
|
+
def assert_validation_error(exc_info, field: str, message_contains: str):
|
|
174
|
+
assert exc_info.type is ValidationError
|
|
175
|
+
assert exc_info.value.field == field
|
|
176
|
+
assert message_contains in str(exc_info.value)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Avoid
|
|
182
|
+
|
|
183
|
+
- All tests in one directory
|
|
184
|
+
- No markers (can't run subsets)
|
|
185
|
+
- Mixed unit/integration in same file
|
|
186
|
+
- Slow tests in unit suite
|
|
187
|
+
- Test order dependencies
|
|
188
|
+
- Duplicate fixtures across directories
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Validation Considerations
|
|
193
|
+
|
|
194
|
+
- `pytest --collect-only | head -20` shows structure
|
|
195
|
+
- `pytest --durations=10` shows slowest tests
|
|
196
|
+
- Coverage targets per test type
|
|
197
|
+
- CI runs unit on every PR, integration nightly
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Related Skills
|
|
202
|
+
|
|
203
|
+
- `testing/fixtures_mocks.md`
|
|
204
|
+
- `testing/parameterized.md`
|
|
205
|
+
- `engineering/virtual_environments.md`
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Testing: Parameterized Tests
|
|
2
|
+
|
|
3
|
+
**Purpose**: Run same test logic with multiple inputs.
|
|
4
|
+
|
|
5
|
+
**When to use**: Testing multiple cases, edge cases, boundary values.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Core Rules
|
|
10
|
+
|
|
11
|
+
### Basic Parametrization
|
|
12
|
+
```python
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
@pytest.mark.parametrize("input,expected", [
|
|
16
|
+
(1, 2),
|
|
17
|
+
(2, 4),
|
|
18
|
+
(3, 6),
|
|
19
|
+
(0, 0),
|
|
20
|
+
(-1, -2),
|
|
21
|
+
])
|
|
22
|
+
def test_double(input: int, expected: int):
|
|
23
|
+
assert double(input) == expected
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Multiple Parameters
|
|
27
|
+
```python
|
|
28
|
+
@pytest.mark.parametrize("a,b,expected", [
|
|
29
|
+
(1, 2, 3),
|
|
30
|
+
(0, 0, 0),
|
|
31
|
+
(-1, 1, 0),
|
|
32
|
+
(100, 200, 300),
|
|
33
|
+
])
|
|
34
|
+
def test_add(a: int, b: int, expected: int):
|
|
35
|
+
assert add(a, b) == expected
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Parametrized Fixtures
|
|
39
|
+
```python
|
|
40
|
+
@pytest.fixture(params=[1, 2, 3, 10, 100])
|
|
41
|
+
def number(request):
|
|
42
|
+
return request.param
|
|
43
|
+
|
|
44
|
+
def test_positive(number: int):
|
|
45
|
+
assert number > 0
|
|
46
|
+
|
|
47
|
+
# Combined with test parametrization
|
|
48
|
+
@pytest.mark.parametrize("multiplier", [2, 3, 10])
|
|
49
|
+
def test_multiply(number: int, multiplier: int):
|
|
50
|
+
assert number * multiplier > 0
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Parametrizing with IDs
|
|
54
|
+
```python
|
|
55
|
+
@pytest.mark.parametrize("email,valid", [
|
|
56
|
+
pytest.param("test@example.com", True, id="valid-standard"),
|
|
57
|
+
pytest.param("user+tag@domain.org", True, id="valid-plus-tag"),
|
|
58
|
+
pytest.param("invalid", False, id="invalid-no-at"),
|
|
59
|
+
pytest.param("@domain.com", False, id="invalid-no-local"),
|
|
60
|
+
pytest.param("user@", False, id="invalid-no-domain"),
|
|
61
|
+
], ids=lambda x: x[1] and "valid" or "invalid")
|
|
62
|
+
def test_email_validation(email: str, valid: bool):
|
|
63
|
+
assert is_valid_email(email) == valid
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Parametrizing Classes
|
|
67
|
+
```python
|
|
68
|
+
@pytest.mark.parametrize("storage_cls", [InMemoryStorage, RedisStorage, FileStorage])
|
|
69
|
+
class TestStorage:
|
|
70
|
+
def test_save_and_load(self, storage_cls):
|
|
71
|
+
storage = storage_cls()
|
|
72
|
+
storage.save("key", "value")
|
|
73
|
+
assert storage.load("key") == "value"
|
|
74
|
+
|
|
75
|
+
def test_delete(self, storage_cls):
|
|
76
|
+
storage = storage_cls()
|
|
77
|
+
storage.save("key", "value")
|
|
78
|
+
storage.delete("key")
|
|
79
|
+
assert storage.load("key") is None
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Dynamic Parametrization
|
|
83
|
+
```python
|
|
84
|
+
def pytest_generate_tests(metafunc):
|
|
85
|
+
"""Hook for dynamic parametrization."""
|
|
86
|
+
if "test_file" in metafunc.fixturenames:
|
|
87
|
+
test_files = list(Path("test_data").glob("*.json"))
|
|
88
|
+
metafunc.parametrize("test_file", test_files, ids=lambda p: p.stem)
|
|
89
|
+
|
|
90
|
+
def test_with_file(test_file: Path):
|
|
91
|
+
data = json.loads(test_file.read_text())
|
|
92
|
+
assert process(data) == data["expected"]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Cartesian Product (Multiple Decorators)
|
|
96
|
+
```python
|
|
97
|
+
@pytest.mark.parametrize("os", ["linux", "windows", "macos"])
|
|
98
|
+
@pytest.mark.parametrize("python", ["3.10", "3.11", "3.12"])
|
|
99
|
+
def test_compatibility(os: str, python: str):
|
|
100
|
+
# Runs 9 combinations
|
|
101
|
+
assert is_compatible(os, python)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Decision Rules
|
|
107
|
+
|
|
108
|
+
| Situation | Pattern |
|
|
109
|
+
|-----------|---------|
|
|
110
|
+
| Few cases (<10) | Inline parametrize |
|
|
111
|
+
| Many cases / data-driven | External data + `pytest_generate_tests` |
|
|
112
|
+
| Cross-product | Multiple `@parametrize` |
|
|
113
|
+
| Fixture variation | Parametrized fixture |
|
|
114
|
+
| Different implementations | Parametrize class |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Preferred Patterns
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
# Boundary value testing
|
|
122
|
+
@pytest.mark.parametrize("value", [
|
|
123
|
+
0, # Minimum
|
|
124
|
+
1, # Just above min
|
|
125
|
+
100, # Typical
|
|
126
|
+
999, # Just below max
|
|
127
|
+
1000, # Maximum
|
|
128
|
+
1001, # Just above max (invalid)
|
|
129
|
+
])
|
|
130
|
+
def test_range_validation(value: int):
|
|
131
|
+
if 0 <= value <= 1000:
|
|
132
|
+
assert validate_range(value) == value
|
|
133
|
+
else:
|
|
134
|
+
with pytest.raises(ValidationError):
|
|
135
|
+
validate_range(value)
|
|
136
|
+
|
|
137
|
+
# Error case parametrization
|
|
138
|
+
ERROR_CASES = [
|
|
139
|
+
(ValidationError, "empty", {}),
|
|
140
|
+
(ValidationError, "missing_field", {"name": "test"}),
|
|
141
|
+
(ConflictError, "duplicate", {"name": "test", "email": "a@b.c"}),
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
@pytest.mark.parametrize("error_type,case_name,input_data", ERROR_CASES)
|
|
145
|
+
def test_create_user_errors(error_type, case_name, input_data):
|
|
146
|
+
with pytest.raises(error_type):
|
|
147
|
+
create_user(**input_data)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Avoid
|
|
153
|
+
|
|
154
|
+
- Too many combinations (exponential explosion)
|
|
155
|
+
- Parametrizing unrelated tests together
|
|
156
|
+
- Complex logic in parametrization (use fixtures instead)
|
|
157
|
+
- Missing `ids` for readability in output
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Validation Considerations
|
|
162
|
+
|
|
163
|
+
- `pytest --collect-only` shows all generated tests
|
|
164
|
+
- `pytest -v` shows parametrized test names
|
|
165
|
+
- `pytest -k "valid"` filters by id/name
|
|
166
|
+
- Duration tracking for large parametrized suites
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Related Skills
|
|
171
|
+
|
|
172
|
+
- `testing/organization.md`
|
|
173
|
+
- `testing/fixtures_mocks.md`
|
|
174
|
+
- `testing/edge_cases.md`
|