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,249 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing_async_tests
|
|
3
|
+
purpose: Testing async code with pytest-asyncio
|
|
4
|
+
category: testing
|
|
5
|
+
triggers:
|
|
6
|
+
- async
|
|
7
|
+
- pytest-asyncio
|
|
8
|
+
- asyncio
|
|
9
|
+
- async test
|
|
10
|
+
- async fixture
|
|
11
|
+
- async mock
|
|
12
|
+
dependencies:
|
|
13
|
+
- generation/async_concurrency.md
|
|
14
|
+
- testing/organization.md
|
|
15
|
+
- testing/fixtures_mocks.md
|
|
16
|
+
- testing/edge_cases.md
|
|
17
|
+
priority: supporting
|
|
18
|
+
estimated_tokens: 2300
|
|
19
|
+
---
|
|
20
|
+
# Testing: Async Tests
|
|
21
|
+
|
|
22
|
+
**Purpose**: Testing async code with pytest-asyncio.
|
|
23
|
+
|
|
24
|
+
**When to use**: Any async functions, services, or integrations.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Core Rules
|
|
29
|
+
|
|
30
|
+
### Setup
|
|
31
|
+
```ini
|
|
32
|
+
# pytest.ini or pyproject.toml
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
asyncio_mode = "auto"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Basic Async Test
|
|
38
|
+
```python
|
|
39
|
+
import pytest
|
|
40
|
+
import pytest_asyncio
|
|
41
|
+
|
|
42
|
+
@pytest.mark.asyncio
|
|
43
|
+
async def test_async_function():
|
|
44
|
+
result = await async_function()
|
|
45
|
+
assert result == "expected"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Async Fixtures
|
|
49
|
+
```python
|
|
50
|
+
# conftest.py
|
|
51
|
+
import pytest_asyncio
|
|
52
|
+
|
|
53
|
+
@pytest_asyncio.fixture
|
|
54
|
+
async def async_client() -> AsyncClient:
|
|
55
|
+
client = AsyncClient()
|
|
56
|
+
yield client
|
|
57
|
+
await client.aclose()
|
|
58
|
+
|
|
59
|
+
@pytest_asyncio.fixture
|
|
60
|
+
async def db_pool() -> asyncpg.Pool:
|
|
61
|
+
pool = await asyncpg.create_pool("postgresql://test:test@localhost/test")
|
|
62
|
+
yield pool
|
|
63
|
+
await pool.close()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Async Mocking
|
|
67
|
+
```python
|
|
68
|
+
from unittest.mock import AsyncMock
|
|
69
|
+
|
|
70
|
+
@pytest.fixture
|
|
71
|
+
def mock_async_service():
|
|
72
|
+
service = AsyncMock()
|
|
73
|
+
service.fetch.return_value = {"data": "test"}
|
|
74
|
+
service.fetch.side_effect = [
|
|
75
|
+
{"data": "first"},
|
|
76
|
+
{"data": "second"},
|
|
77
|
+
Exception("error"),
|
|
78
|
+
]
|
|
79
|
+
return service
|
|
80
|
+
|
|
81
|
+
@pytest.mark.asyncio
|
|
82
|
+
async def test_async_mock(mock_async_service):
|
|
83
|
+
result = await mock_async_service.fetch()
|
|
84
|
+
assert result == {"data": "first"}
|
|
85
|
+
mock_async_service.fetch.assert_awaited_once()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Testing Async Generators
|
|
89
|
+
```python
|
|
90
|
+
@pytest.mark.asyncio
|
|
91
|
+
async def test_async_generator():
|
|
92
|
+
results = []
|
|
93
|
+
async for item in async_generator():
|
|
94
|
+
results.append(item)
|
|
95
|
+
assert results == [1, 2, 3]
|
|
96
|
+
|
|
97
|
+
# Or collect
|
|
98
|
+
@pytest.mark.asyncio
|
|
99
|
+
async def test_async_generator_collect():
|
|
100
|
+
items = [item async for item in async_generator()]
|
|
101
|
+
assert items == [1, 2, 3]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Testing Timeouts
|
|
105
|
+
```python
|
|
106
|
+
import asyncio
|
|
107
|
+
|
|
108
|
+
@pytest.mark.asyncio
|
|
109
|
+
async def test_timeout():
|
|
110
|
+
async def slow():
|
|
111
|
+
await asyncio.sleep(10)
|
|
112
|
+
return "done"
|
|
113
|
+
|
|
114
|
+
with pytest.raises(asyncio.TimeoutError):
|
|
115
|
+
await asyncio.wait_for(slow(), timeout=0.1)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Testing Concurrency
|
|
119
|
+
```python
|
|
120
|
+
@pytest.mark.asyncio
|
|
121
|
+
async def test_concurrent_requests():
|
|
122
|
+
async def fetch(url):
|
|
123
|
+
await asyncio.sleep(0.1)
|
|
124
|
+
return url
|
|
125
|
+
|
|
126
|
+
urls = [f"http://example.com/{i}" for i in range(10)]
|
|
127
|
+
results = await asyncio.gather(*[fetch(u) for u in urls])
|
|
128
|
+
assert len(results) == 10
|
|
129
|
+
|
|
130
|
+
# With semaphore
|
|
131
|
+
@pytest.mark.asyncio
|
|
132
|
+
async def test_rate_limited():
|
|
133
|
+
sem = asyncio.Semaphore(2)
|
|
134
|
+
|
|
135
|
+
async def limited_fetch(url):
|
|
136
|
+
async with sem:
|
|
137
|
+
await asyncio.sleep(0.1)
|
|
138
|
+
return url
|
|
139
|
+
|
|
140
|
+
# Should take ~0.5s (10 tasks / 2 concurrent * 0.1s)
|
|
141
|
+
import time
|
|
142
|
+
start = time.monotonic()
|
|
143
|
+
await asyncio.gather(*[limited_fetch(f"url{i}") for i in range(10)])
|
|
144
|
+
elapsed = time.monotonic() - start
|
|
145
|
+
assert 0.4 < elapsed < 0.7
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Testing Cancellation
|
|
149
|
+
```python
|
|
150
|
+
@pytest.mark.asyncio
|
|
151
|
+
async def test_cancellation():
|
|
152
|
+
async def long_running():
|
|
153
|
+
try:
|
|
154
|
+
await asyncio.sleep(10)
|
|
155
|
+
except asyncio.CancelledError:
|
|
156
|
+
# Cleanup
|
|
157
|
+
raise
|
|
158
|
+
|
|
159
|
+
task = asyncio.create_task(long_running())
|
|
160
|
+
await asyncio.sleep(0.01)
|
|
161
|
+
task.cancel()
|
|
162
|
+
|
|
163
|
+
with pytest.raises(asyncio.CancelledError):
|
|
164
|
+
await task
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### TaskGroup (Python 3.11+)
|
|
168
|
+
```python
|
|
169
|
+
@pytest.mark.asyncio
|
|
170
|
+
async def test_task_group():
|
|
171
|
+
async with asyncio.TaskGroup() as tg:
|
|
172
|
+
task1 = tg.create_task(fetch("url1"))
|
|
173
|
+
task2 = tg.create_task(fetch("url2"))
|
|
174
|
+
|
|
175
|
+
# All completed or exception raised
|
|
176
|
+
assert task1.result() == "url1"
|
|
177
|
+
assert task2.result() == "url2"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Decision Rules
|
|
183
|
+
|
|
184
|
+
| Need | Pattern |
|
|
185
|
+
|------|---------|
|
|
186
|
+
| Async function | `@pytest.mark.asyncio` |
|
|
187
|
+
| Async fixture | `@pytest_asyncio.fixture` |
|
|
188
|
+
| External async service | `AsyncMock` |
|
|
189
|
+
| Timeout behavior | `asyncio.wait_for` |
|
|
190
|
+
| Cancellation | `task.cancel()` + `CancelledError` |
|
|
191
|
+
| Concurrent behavior | `asyncio.gather` / `TaskGroup` |
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Preferred Patterns
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
# Test with real async dependencies (integration)
|
|
199
|
+
@pytest.mark.asyncio
|
|
200
|
+
async def test_database_integration(db_pool):
|
|
201
|
+
async with db_pool.acquire() as conn:
|
|
202
|
+
await conn.execute("INSERT INTO test (value) VALUES ($1)", "test")
|
|
203
|
+
row = await conn.fetchrow("SELECT value FROM test WHERE value = $1", "test")
|
|
204
|
+
assert row["value"] == "test"
|
|
205
|
+
|
|
206
|
+
# Test retry logic
|
|
207
|
+
@pytest.mark.asyncio
|
|
208
|
+
async def test_retry_on_failure():
|
|
209
|
+
call_count = 0
|
|
210
|
+
|
|
211
|
+
async def flaky():
|
|
212
|
+
nonlocal call_count
|
|
213
|
+
call_count += 1
|
|
214
|
+
if call_count < 3:
|
|
215
|
+
raise ConnectionError("fail")
|
|
216
|
+
return "success"
|
|
217
|
+
|
|
218
|
+
result = await retry_async(flaky, attempts=3, base_delay=0.01)
|
|
219
|
+
assert result == "success"
|
|
220
|
+
assert call_count == 3
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Avoid
|
|
226
|
+
|
|
227
|
+
- Sync tests for async code
|
|
228
|
+
- `asyncio.run()` in tests (use pytest-asyncio)
|
|
229
|
+
- Blocking sleeps (`time.sleep`) in async tests
|
|
230
|
+
- Not awaiting mocks (`assert_awaited` vs `assert_called`)
|
|
231
|
+
- Shared event loop state between tests
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Validation Considerations
|
|
236
|
+
|
|
237
|
+
- `pytest-asyncio` handles event loop per test
|
|
238
|
+
- No "Event loop is closed" errors
|
|
239
|
+
- Timeout tests actually timeout (not hang)
|
|
240
|
+
- Cancellation cleanup verified
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Related Skills
|
|
245
|
+
|
|
246
|
+
- `generation/async_concurrency.md`
|
|
247
|
+
- `testing/organization.md`
|
|
248
|
+
- `testing/fixtures_mocks.md`
|
|
249
|
+
- `testing/edge_cases.md`
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Testing: Coverage
|
|
2
|
+
|
|
3
|
+
**Purpose**: Meaningful test coverage measurement and targets.
|
|
4
|
+
|
|
5
|
+
**When to use**: Configuring coverage, interpreting reports, setting thresholds.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Core Rules
|
|
10
|
+
|
|
11
|
+
### Coverage Types
|
|
12
|
+
| Type | Meaning |
|
|
13
|
+
|------|---------|
|
|
14
|
+
| **Line** | Executable lines executed |
|
|
15
|
+
| **Branch** | Decision branches taken (if/else, loops) |
|
|
16
|
+
| **Function** | Functions called |
|
|
17
|
+
| **Statement** | Similar to line |
|
|
18
|
+
|
|
19
|
+
### Configuration
|
|
20
|
+
```toml
|
|
21
|
+
# pyproject.toml
|
|
22
|
+
[tool.coverage.run]
|
|
23
|
+
source = ["src"]
|
|
24
|
+
omit = [
|
|
25
|
+
"tests/*",
|
|
26
|
+
"*/__main__.py",
|
|
27
|
+
"*/migrations/*",
|
|
28
|
+
"*/conftest.py",
|
|
29
|
+
]
|
|
30
|
+
branch = true # Branch coverage (important!)
|
|
31
|
+
|
|
32
|
+
[tool.coverage.report]
|
|
33
|
+
exclude_lines = [
|
|
34
|
+
"pragma: no cover",
|
|
35
|
+
"def __repr__",
|
|
36
|
+
"raise AssertionError",
|
|
37
|
+
"raise NotImplementedError",
|
|
38
|
+
"if __name__ == .__main__.:",
|
|
39
|
+
"if TYPE_CHECKING:",
|
|
40
|
+
]
|
|
41
|
+
precision = 2
|
|
42
|
+
show_missing = true
|
|
43
|
+
|
|
44
|
+
[tool.coverage.html]
|
|
45
|
+
directory = "htmlcov"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Running Coverage
|
|
49
|
+
```bash
|
|
50
|
+
# Basic
|
|
51
|
+
pytest --cov=mypackage --cov-report=term-missing
|
|
52
|
+
|
|
53
|
+
# With branch coverage
|
|
54
|
+
pytest --cov=mypackage --cov-branch --cov-report=term-missing
|
|
55
|
+
|
|
56
|
+
# HTML report
|
|
57
|
+
pytest --cov=mypackage --cov-report=html
|
|
58
|
+
|
|
59
|
+
# XML for CI
|
|
60
|
+
pytest --cov=mypackage --cov-report=xml
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Interpreting Coverage
|
|
64
|
+
```text
|
|
65
|
+
Name Stmts Miss Branch BrMiss Cover Missing
|
|
66
|
+
----------------------------------------------------------------------
|
|
67
|
+
src/mypackage/__init__ 5 0 0 0 100%
|
|
68
|
+
src/mypackage/models.py 50 2 10 2 92% 45-46
|
|
69
|
+
src/mypackage/service.py 120 15 30 8 85% 78-85, 92-95
|
|
70
|
+
src/mypackage/utils.py 30 0 4 0 100%
|
|
71
|
+
----------------------------------------------------------------------
|
|
72
|
+
TOTAL 205 17 44 10 90%
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- **Line coverage**: 90% (188/205)
|
|
76
|
+
- **Branch coverage**: 77% (34/44) — more important!
|
|
77
|
+
- **Missing**: Shows exact lines/branches not covered
|
|
78
|
+
|
|
79
|
+
### Coverage Targets
|
|
80
|
+
| Project Type | Line | Branch |
|
|
81
|
+
|--------------|------|--------|
|
|
82
|
+
| New project | 90%+ | 85%+ |
|
|
83
|
+
| Mature library | 95%+ | 90%+ |
|
|
84
|
+
| Application | 80%+ | 75%+ |
|
|
85
|
+
| Legacy | Improve incrementally | |
|
|
86
|
+
|
|
87
|
+
### What Coverage Does NOT Measure
|
|
88
|
+
- **Logic correctness** — 100% coverage ≠ bug-free
|
|
89
|
+
- **Edge cases** — May cover line but not all values
|
|
90
|
+
- **Integration** — Unit coverage ≠ system works
|
|
91
|
+
- **Security** — Coverage doesn't check vulnerabilities
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Decision Rules
|
|
96
|
+
|
|
97
|
+
| Situation | Coverage Goal |
|
|
98
|
+
|-----------|---------------|
|
|
99
|
+
| New code (PR) | 100% line, 100% branch |
|
|
100
|
+
| Critical paths (auth, payments) | 100% branch |
|
|
101
|
+
| Legacy code | Incremental improvement |
|
|
102
|
+
| Generated code | Exclude (`pragma: no cover`) |
|
|
103
|
+
| Prototypes | No requirement |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Preferred Patterns
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# Exclude from coverage (rare, justify)
|
|
111
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
112
|
+
return f"<{self.__class__.__name__}(id={self.id})>"
|
|
113
|
+
|
|
114
|
+
# Hard to test error paths
|
|
115
|
+
def connect(self): # pragma: no cover
|
|
116
|
+
try:
|
|
117
|
+
self._connect()
|
|
118
|
+
except OSError as e:
|
|
119
|
+
if e.errno == errno.ENETUNREACH:
|
|
120
|
+
raise NetworkUnreachable() from e
|
|
121
|
+
raise
|
|
122
|
+
|
|
123
|
+
# Branch coverage important here
|
|
124
|
+
def process(value: int) -> str:
|
|
125
|
+
if value > 0: # Branch 1
|
|
126
|
+
return "positive"
|
|
127
|
+
elif value < 0: # Branch 2
|
|
128
|
+
return "negative"
|
|
129
|
+
else: # Branch 3
|
|
130
|
+
return "zero"
|
|
131
|
+
|
|
132
|
+
# Test all 3 branches
|
|
133
|
+
@pytest.mark.parametrize("value,expected", [
|
|
134
|
+
(1, "positive"),
|
|
135
|
+
(-1, "negative"),
|
|
136
|
+
(0, "zero"),
|
|
137
|
+
])
|
|
138
|
+
def test_process(value, expected):
|
|
139
|
+
assert process(value) == expected
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Avoid
|
|
145
|
+
|
|
146
|
+
- Coverage as only quality metric
|
|
147
|
+
- 100% coverage mandate (leads to bad tests)
|
|
148
|
+
- Excluding code without justification
|
|
149
|
+
- Not measuring branch coverage
|
|
150
|
+
- Testing getters/setters just for coverage
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Validation Considerations
|
|
155
|
+
|
|
156
|
+
- CI fails if coverage drops below threshold
|
|
157
|
+
- `pytest --cov-fail-under=80 --cov-branch --cov-fail-under-branch=75`
|
|
158
|
+
- Track coverage trends over time
|
|
159
|
+
- Focus on branch coverage for critical logic
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Related Skills
|
|
164
|
+
|
|
165
|
+
- `testing/organization.md`
|
|
166
|
+
- `testing/edge_cases.md`
|
|
167
|
+
- `testing/regression_tests.md`
|
|
168
|
+
- `quality/maintainability.md`
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Testing: Edge Cases
|
|
2
|
+
|
|
3
|
+
**Purpose**: Systematic edge case coverage for robust code.
|
|
4
|
+
|
|
5
|
+
**When to use**: Writing tests for any function with external input or complex logic.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Core Rules
|
|
10
|
+
|
|
11
|
+
### Edge Case Categories
|
|
12
|
+
|
|
13
|
+
| Category | Examples |
|
|
14
|
+
|----------|----------|
|
|
15
|
+
| **Empty/Zero** | `""`, `[]`, `{}`, `0`, `0.0`, `None` |
|
|
16
|
+
| **Boundary** | Min, max, min-1, max+1, off-by-one |
|
|
17
|
+
| **Special values** | `NaN`, `Inf`, `-0.0`, `True`/`False` as int |
|
|
18
|
+
| **Unicode** | Emoji, RTL, combining chars, null bytes |
|
|
19
|
+
| **Large** | Huge strings, deep nesting, many items |
|
|
20
|
+
| **Concurrent** | Race conditions, double-submit |
|
|
21
|
+
| **Failure** | Network error, timeout, disk full, permission denied |
|
|
22
|
+
| **Malformed** | Invalid encoding, truncated data, wrong type |
|
|
23
|
+
|
|
24
|
+
### Parameterized Edge Cases
|
|
25
|
+
```python
|
|
26
|
+
import pytest
|
|
27
|
+
|
|
28
|
+
# String edges
|
|
29
|
+
STRING_EDGES = [
|
|
30
|
+
("", "empty"),
|
|
31
|
+
(" ", "whitespace"),
|
|
32
|
+
("\t\n\r", "control-chars"),
|
|
33
|
+
("a" * 10000, "long"),
|
|
34
|
+
("🎉🎊", "emoji"),
|
|
35
|
+
("\u200b", "zero-width"),
|
|
36
|
+
("\x00", "null-byte"),
|
|
37
|
+
("../../../etc/passwd", "traversal"),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize("value,case_id", STRING_EDGES, ids=lambda x: x[1])
|
|
41
|
+
def test_string_input(value: str, case_id: str):
|
|
42
|
+
result = process_string(value)
|
|
43
|
+
assert isinstance(result, str)
|
|
44
|
+
|
|
45
|
+
# Numeric edges
|
|
46
|
+
NUMERIC_EDGES = [
|
|
47
|
+
(0, "zero"),
|
|
48
|
+
(-1, "negative"),
|
|
49
|
+
(1, "positive"),
|
|
50
|
+
(2**63 - 1, "max-int64"),
|
|
51
|
+
(-(2**63), "min-int64"),
|
|
52
|
+
(float("inf"), "infinity"),
|
|
53
|
+
(float("-inf"), "neg-infinity"),
|
|
54
|
+
(float("nan"), "nan"),
|
|
55
|
+
(1.5, "float"),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
@pytest.mark.parametrize("value,case_id", NUMERIC_EDGES, ids=lambda x: x[1])
|
|
59
|
+
def test_numeric_input(value, case_id):
|
|
60
|
+
if isinstance(value, float) and (value != value or value in (float("inf"), float("-inf"))):
|
|
61
|
+
with pytest.raises(ValidationError):
|
|
62
|
+
process_number(value)
|
|
63
|
+
else:
|
|
64
|
+
result = process_number(value)
|
|
65
|
+
assert result == expected(value)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Collection Edges
|
|
69
|
+
```python
|
|
70
|
+
COLLECTION_EDGES = [
|
|
71
|
+
([], "empty"),
|
|
72
|
+
([1], "single"),
|
|
73
|
+
([1, 2], "two"),
|
|
74
|
+
(list(range(10000)), "large"),
|
|
75
|
+
([None, "", 0, False], "falsy-values"),
|
|
76
|
+
([{"nested": {"deep": "value"}}], "nested"),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
@pytest.mark.parametrize("items,case_id", COLLECTION_EDGES, ids=lambda x: x[1])
|
|
80
|
+
def test_collection_processing(items, case_id):
|
|
81
|
+
result = process_items(items)
|
|
82
|
+
assert len(result) == len(items)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### None/Optional Edges
|
|
86
|
+
```python
|
|
87
|
+
def test_optional_handling():
|
|
88
|
+
# Explicit None
|
|
89
|
+
assert process_optional(None) == default_value()
|
|
90
|
+
|
|
91
|
+
# Missing key vs None value
|
|
92
|
+
assert process_dict({}) == default_value()
|
|
93
|
+
assert process_dict({"key": None}) == default_value()
|
|
94
|
+
assert process_dict({"key": "value"}) == "value"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Concurrency Edges
|
|
98
|
+
```python
|
|
99
|
+
import threading
|
|
100
|
+
import time
|
|
101
|
+
|
|
102
|
+
def test_thread_safety(counter):
|
|
103
|
+
def increment():
|
|
104
|
+
for _ in range(1000):
|
|
105
|
+
counter.increment()
|
|
106
|
+
|
|
107
|
+
threads = [threading.Thread(target=increment) for _ in range(10)]
|
|
108
|
+
for t in threads: t.start()
|
|
109
|
+
for t in threads: t.join()
|
|
110
|
+
|
|
111
|
+
assert counter.value == 10000 # No race condition
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Time Edges
|
|
115
|
+
```python
|
|
116
|
+
from freezegun import freeze_time
|
|
117
|
+
|
|
118
|
+
@freeze_time("2024-01-15 12:00:00")
|
|
119
|
+
def test_time_dependent():
|
|
120
|
+
assert get_current_timestamp() == "2024-01-15T12:00:00Z"
|
|
121
|
+
|
|
122
|
+
# Leap year, DST, timezone
|
|
123
|
+
@freeze_time("2024-02-29 23:59:59") # Leap day
|
|
124
|
+
def test_leap_year():
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
@freeze_time("2024-03-10 02:30:00", tz_offset=-5) # DST transition
|
|
128
|
+
def test_dst():
|
|
129
|
+
...
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Decision Rules
|
|
135
|
+
|
|
136
|
+
| Function Input | Edge Cases to Test |
|
|
137
|
+
|----------------|-------------------|
|
|
138
|
+
| String | Empty, whitespace, unicode, long, injection |
|
|
139
|
+
| Number | 0, negative, min/max, float special |
|
|
140
|
+
| List/Dict | Empty, single, large, nested, None elements |
|
|
141
|
+
| Optional | None, missing, empty string |
|
|
142
|
+
| Date/Time | Boundaries, DST, leap, timezone |
|
|
143
|
+
| File | Empty, large, missing, permission, symlink |
|
|
144
|
+
| Network | Timeout, 5xx, 4xx, malformed response |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Preferred Patterns
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
# Property-based testing (hypothesis)
|
|
152
|
+
from hypothesis import given, strategies as st
|
|
153
|
+
|
|
154
|
+
@given(st.text())
|
|
155
|
+
def test_string_property(s: str):
|
|
156
|
+
# Should never crash on any string
|
|
157
|
+
result = safe_process(s)
|
|
158
|
+
assert isinstance(result, str)
|
|
159
|
+
|
|
160
|
+
@given(st.integers())
|
|
161
|
+
def test_int_property(n: int):
|
|
162
|
+
result = process_int(n)
|
|
163
|
+
assert result >= 0
|
|
164
|
+
|
|
165
|
+
@given(st.lists(st.integers()))
|
|
166
|
+
def test_list_property(items: list[int]):
|
|
167
|
+
result = process_list(items)
|
|
168
|
+
assert len(result) <= len(items)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Avoid
|
|
174
|
+
|
|
175
|
+
- Only testing "happy path"
|
|
176
|
+
- Assuming input is always valid
|
|
177
|
+
- Missing boundary values (off-by-one)
|
|
178
|
+
- Not testing error handling paths
|
|
179
|
+
- Ignoring Unicode/timezone/concurrency
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Validation Considerations
|
|
184
|
+
|
|
185
|
+
- `pytest --cov` shows uncovered branches
|
|
186
|
+
- `hypothesis` finds unexpected edge cases
|
|
187
|
+
- Mutation testing (`mutmut`) verifies test quality
|
|
188
|
+
- Fuzzing for parsers/decoders
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Related Skills
|
|
193
|
+
|
|
194
|
+
- `testing/organization.md`
|
|
195
|
+
- `testing/parameterized.md`
|
|
196
|
+
- `security/input_validation.md`
|
|
197
|
+
- `generation/error_handling.md`
|