ndomo 0.1.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.
- package/.bun-version +1 -0
- package/.dockerignore +79 -0
- package/.editorconfig +18 -0
- package/.env.example +19 -0
- package/.github/CODEOWNERS +8 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
- package/.github/ISSUE_TEMPLATE/config.yml +2 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +34 -0
- package/.github/dependabot.yml +36 -0
- package/.github/pull_request_template.md +24 -0
- package/.github/release.yml +30 -0
- package/.github/workflows/gitleaks.yml +28 -0
- package/.github/workflows/release-please.yml +27 -0
- package/.github/workflows/smoke.yml +29 -0
- package/.husky/commit-msg +1 -0
- package/CHANGELOG.md +114 -0
- package/Dockerfile +32 -0
- package/README.es.md +174 -0
- package/README.md +187 -0
- package/agents/chronicler.md +98 -0
- package/agents/ci-smith.md +136 -0
- package/agents/craftsman.md +341 -0
- package/agents/deploy-smith.md +138 -0
- package/agents/foreman.md +377 -0
- package/agents/go-smith.md +164 -0
- package/agents/guild.md +188 -0
- package/agents/inspector.md +83 -0
- package/agents/js-smith.md +127 -0
- package/agents/ops-scout.md +173 -0
- package/agents/painter.md +200 -0
- package/agents/python-smith.md +120 -0
- package/agents/ranger.md +307 -0
- package/agents/release-smith.md +165 -0
- package/agents/rust-smith.md +159 -0
- package/agents/sage.md +178 -0
- package/agents/scout.md +144 -0
- package/agents/scribe.md +156 -0
- package/agents/smith.md +201 -0
- package/agents/vue-smith.md +155 -0
- package/agents/warden.md +216 -0
- package/agents/zig-smith.md +156 -0
- package/bin/ndomo-analyses.ts +4 -0
- package/bin/ndomo-status.ts +4 -0
- package/biome.json +57 -0
- package/bun.lock +514 -0
- package/commitlint.config.js +3 -0
- package/config/ndomo.config.json +258 -0
- package/config/ndomo.schema.json +166 -0
- package/docs/agents.md +375 -0
- package/docs/bugs/plan-create-orphan-fk.md +131 -0
- package/docs/bugs/task_create_batch-order-index-collision.md +158 -0
- package/docs/configuration.md +276 -0
- package/docs/database.md +364 -0
- package/docs/features/feature-flexible-builder-v1.md +724 -0
- package/docs/features/feature-flexible-builder-v2.md +882 -0
- package/docs/features/feature-flexible-builder.md +974 -0
- package/docs/http-server.md +244 -0
- package/docs/installation.md +259 -0
- package/docs/integrations.md +129 -0
- package/docs/operations/anti-pattern-sub-agent-verify-2026-06-21.md +32 -0
- package/docs/operations/audit-v1.md +417 -0
- package/docs/operations/audit-v2.md +197 -0
- package/docs/operations/audit-v3.md +306 -0
- package/docs/operations/db-optimize-foundations.md +123 -0
- package/docs/operations/verify-gate-architecture.md +82 -0
- package/docs/workflows.md +448 -0
- package/opencode.json +5 -0
- package/package.json +65 -0
- package/release-please-config.json +11 -0
- package/scripts/dev-bust-cache.sh +164 -0
- package/scripts/install.sh +688 -0
- package/scripts/smoke-e2e.ts +704 -0
- package/scripts/smoke-hot.ts +417 -0
- package/scripts/smoke-http.sh +228 -0
- package/scripts/smoke-v4.ts +256 -0
- package/scripts/smoke-v5.ts +397 -0
- package/scripts/smoke.sh +9 -0
- package/scripts/uninstall.sh +224 -0
- package/skills/api-security-best-practices/SKILL.md +915 -0
- package/skills/bash-scripting/SKILL.md +201 -0
- package/skills/bun/SKILL.md +313 -0
- package/skills/cavecrew/SKILL.md +82 -0
- package/skills/caveman/SKILL.md +74 -0
- package/skills/caveman-review/README.md +33 -0
- package/skills/caveman-review/SKILL.md +55 -0
- package/skills/find-skills/SKILL.md +142 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/SKILL.md +55 -0
- package/skills/golang-patterns/SKILL.md +674 -0
- package/skills/golang-security/SKILL.md +185 -0
- package/skills/golang-security/evals/evals.json +595 -0
- package/skills/golang-security/references/architecture.md +268 -0
- package/skills/golang-security/references/checklist.md +80 -0
- package/skills/golang-security/references/cookies.md +200 -0
- package/skills/golang-security/references/cryptography.md +424 -0
- package/skills/golang-security/references/filesystem.md +285 -0
- package/skills/golang-security/references/injection.md +315 -0
- package/skills/golang-security/references/logging.md +163 -0
- package/skills/golang-security/references/memory-safety.md +241 -0
- package/skills/golang-security/references/network.md +253 -0
- package/skills/golang-security/references/secrets.md +189 -0
- package/skills/golang-security/references/third-party.md +159 -0
- package/skills/golang-security/references/threat-modeling.md +189 -0
- package/skills/golang-testing/SKILL.md +720 -0
- package/skills/grill-me/SKILL.md +7 -0
- package/skills/javascript-testing-patterns/SKILL.md +537 -0
- package/skills/javascript-testing-patterns/references/advanced-testing-patterns.md +513 -0
- package/skills/modern-javascript-patterns/SKILL.md +43 -0
- package/skills/modern-javascript-patterns/references/advanced-patterns.md +487 -0
- package/skills/modern-javascript-patterns/references/details.md +457 -0
- package/skills/python-anti-patterns/SKILL.md +349 -0
- package/skills/python-design-patterns/SKILL.md +85 -0
- package/skills/python-design-patterns/references/details.md +353 -0
- package/skills/python-error-handling/SKILL.md +193 -0
- package/skills/python-error-handling/references/details.md +171 -0
- package/skills/python-testing-patterns/SKILL.md +278 -0
- package/skills/python-testing-patterns/references/advanced-patterns.md +411 -0
- package/skills/python-testing-patterns/references/details.md +349 -0
- package/skills/rust-patterns/SKILL.md +500 -0
- package/skills/rust-testing/SKILL.md +501 -0
- package/skills/security-review/SKILL.md +504 -0
- package/skills/security-review/cloud-infrastructure-security.md +361 -0
- package/skills/vue-best-practices/SKILL.md +154 -0
- package/skills/vue-best-practices/references/animation-class-based-technique.md +254 -0
- package/skills/vue-best-practices/references/animation-state-driven-technique.md +291 -0
- package/skills/vue-best-practices/references/component-async.md +97 -0
- package/skills/vue-best-practices/references/component-data-flow.md +307 -0
- package/skills/vue-best-practices/references/component-fallthrough-attrs.md +174 -0
- package/skills/vue-best-practices/references/component-keep-alive.md +137 -0
- package/skills/vue-best-practices/references/component-slots.md +216 -0
- package/skills/vue-best-practices/references/component-suspense.md +228 -0
- package/skills/vue-best-practices/references/component-teleport.md +108 -0
- package/skills/vue-best-practices/references/component-transition-group.md +128 -0
- package/skills/vue-best-practices/references/component-transition.md +125 -0
- package/skills/vue-best-practices/references/composables.md +290 -0
- package/skills/vue-best-practices/references/directives.md +162 -0
- package/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +159 -0
- package/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +182 -0
- package/skills/vue-best-practices/references/perf-virtualize-large-lists.md +187 -0
- package/skills/vue-best-practices/references/plugins.md +166 -0
- package/skills/vue-best-practices/references/reactivity.md +344 -0
- package/skills/vue-best-practices/references/render-functions.md +201 -0
- package/skills/vue-best-practices/references/sfc.md +310 -0
- package/skills/vue-best-practices/references/state-management.md +135 -0
- package/skills/vue-best-practices/references/updated-hook-performance.md +187 -0
- package/skills/vue-pinia-best-practices/SKILL.md +21 -0
- package/skills/vue-pinia-best-practices/reference/pinia-no-active-pinia-error.md +248 -0
- package/skills/vue-pinia-best-practices/reference/pinia-setup-store-return-all-state.md +227 -0
- package/skills/vue-pinia-best-practices/reference/pinia-store-destructuring-breaks-reactivity.md +193 -0
- package/skills/vue-pinia-best-practices/reference/state-url-for-ephemeral-filters.md +238 -0
- package/skills/vue-pinia-best-practices/reference/state-use-pinia-for-large-apps.md +262 -0
- package/skills/vue-pinia-best-practices/reference/store-method-binding-parentheses.md +191 -0
- package/skills/zig-0.16/SKILL.md +840 -0
- package/skills/zig-0.16/scripts/check-zig-version.sh +21 -0
- package/src/cli/analyses.ts +280 -0
- package/src/cli/index.ts +108 -0
- package/src/cli/serve.ts +192 -0
- package/src/cli/smoke.ts +131 -0
- package/src/cli/status.test.ts +204 -0
- package/src/cli/status.ts +263 -0
- package/src/cli/vacuum.test.ts +82 -0
- package/src/cli/vacuum.ts +96 -0
- package/src/config/schema.test.ts +88 -0
- package/src/config/schema.ts +64 -0
- package/src/db/analyses-migration.test.ts +210 -0
- package/src/db/analyses.test.ts +466 -0
- package/src/db/analyses.ts +375 -0
- package/src/db/auto-checkpoint.ts +131 -0
- package/src/db/client.test.ts +129 -0
- package/src/db/client.ts +55 -0
- package/src/db/fts-escape.ts +20 -0
- package/src/db/incidents.test.ts +201 -0
- package/src/db/incidents.ts +93 -0
- package/src/db/index.ts +86 -0
- package/src/db/migrations-v13.test.ts +141 -0
- package/src/db/migrations-v8.test.ts +301 -0
- package/src/db/migrations.ts +147 -0
- package/src/db/plan-archive.test.ts +180 -0
- package/src/db/plan-archive.ts +274 -0
- package/src/db/plan-create.test.ts +276 -0
- package/src/db/plan-create.ts +78 -0
- package/src/db/plan-files.test.ts +289 -0
- package/src/db/plan-update-status.ts +287 -0
- package/src/db/plans.test.ts +490 -0
- package/src/db/plans.ts +534 -0
- package/src/db/resolve-project-dir.test.ts +143 -0
- package/src/db/resolve-project-dir.ts +75 -0
- package/src/db/rollbacks.test.ts +150 -0
- package/src/db/rollbacks.ts +67 -0
- package/src/db/schema.ts +907 -0
- package/src/db/sessions.test.ts +80 -0
- package/src/db/sessions.ts +135 -0
- package/src/db/shutdown.test.ts +147 -0
- package/src/db/shutdown.ts +45 -0
- package/src/db/tasks.test.ts +921 -0
- package/src/db/tasks.ts +747 -0
- package/src/db/types.ts +619 -0
- package/src/http/__tests__/auth.test.ts +196 -0
- package/src/http/__tests__/routes.test.ts +465 -0
- package/src/http/__tests__/sse.test.ts +317 -0
- package/src/http/auth.ts +72 -0
- package/src/http/middleware/cors.ts +53 -0
- package/src/http/middleware/security-headers.ts +21 -0
- package/src/http/routes/events.ts +112 -0
- package/src/http/routes/health.ts +51 -0
- package/src/http/routes/plans.ts +66 -0
- package/src/http/routes/sessions.ts +50 -0
- package/src/http/routes/tasks.ts +60 -0
- package/src/http/server.ts +95 -0
- package/src/http/sse.ts +116 -0
- package/src/index.ts +37 -0
- package/src/lib.ts +65 -0
- package/src/mem/scoped.ts +65 -0
- package/src/orchestrator/background.test.ts +268 -0
- package/src/orchestrator/background.ts +293 -0
- package/src/orchestrator/memory-hook.ts +182 -0
- package/src/orchestrator/reconciler.ts +123 -0
- package/src/orchestrator/scheduler.test.ts +300 -0
- package/src/orchestrator/scheduler.ts +243 -0
- package/src/plugin.test.ts +2574 -0
- package/src/plugin.ts +1690 -0
- package/src/sdk/client.ts +66 -0
- package/src/worktrees/manager.ts +236 -0
- package/src/worktrees/state.ts +87 -0
- package/tests/integration/ranger-flow.test.ts +257 -0
- package/tools/analysis_archive.ts +28 -0
- package/tools/analysis_create.ts +55 -0
- package/tools/analysis_get.ts +33 -0
- package/tools/analysis_link_plan.ts +44 -0
- package/tools/analysis_list.ts +48 -0
- package/tools/analysis_search.ts +36 -0
- package/tools/analysis_update.ts +44 -0
- package/tools/plan_approve.ts +31 -0
- package/tools/plan_create.ts +58 -0
- package/tools/plan_get.ts +40 -0
- package/tools/plan_list.ts +37 -0
- package/tools/plan_search.ts +34 -0
- package/tools/plan_update_status.ts +71 -0
- package/tools/session_checkpoint.ts +31 -0
- package/tools/session_end.ts +26 -0
- package/tools/session_start.ts +43 -0
- package/tools/task_create_batch.ts +70 -0
- package/tools/task_list.ts +35 -0
- package/tools/task_next_for_agent.ts +30 -0
- package/tools/task_search.ts +34 -0
- package/tools/task_update_status.ts +37 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# python-testing-patterns — detailed patterns and worked examples
|
|
2
|
+
|
|
3
|
+
## Fundamental Patterns
|
|
4
|
+
|
|
5
|
+
### Pattern 1: Basic pytest Tests
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
# test_calculator.py
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
class Calculator:
|
|
12
|
+
"""Simple calculator for testing."""
|
|
13
|
+
|
|
14
|
+
def add(self, a: float, b: float) -> float:
|
|
15
|
+
return a + b
|
|
16
|
+
|
|
17
|
+
def subtract(self, a: float, b: float) -> float:
|
|
18
|
+
return a - b
|
|
19
|
+
|
|
20
|
+
def multiply(self, a: float, b: float) -> float:
|
|
21
|
+
return a * b
|
|
22
|
+
|
|
23
|
+
def divide(self, a: float, b: float) -> float:
|
|
24
|
+
if b == 0:
|
|
25
|
+
raise ValueError("Cannot divide by zero")
|
|
26
|
+
return a / b
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_addition():
|
|
30
|
+
"""Test addition."""
|
|
31
|
+
calc = Calculator()
|
|
32
|
+
assert calc.add(2, 3) == 5
|
|
33
|
+
assert calc.add(-1, 1) == 0
|
|
34
|
+
assert calc.add(0, 0) == 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_subtraction():
|
|
38
|
+
"""Test subtraction."""
|
|
39
|
+
calc = Calculator()
|
|
40
|
+
assert calc.subtract(5, 3) == 2
|
|
41
|
+
assert calc.subtract(0, 5) == -5
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_multiplication():
|
|
45
|
+
"""Test multiplication."""
|
|
46
|
+
calc = Calculator()
|
|
47
|
+
assert calc.multiply(3, 4) == 12
|
|
48
|
+
assert calc.multiply(0, 5) == 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_division():
|
|
52
|
+
"""Test division."""
|
|
53
|
+
calc = Calculator()
|
|
54
|
+
assert calc.divide(6, 3) == 2
|
|
55
|
+
assert calc.divide(5, 2) == 2.5
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_division_by_zero():
|
|
59
|
+
"""Test division by zero raises error."""
|
|
60
|
+
calc = Calculator()
|
|
61
|
+
with pytest.raises(ValueError, match="Cannot divide by zero"):
|
|
62
|
+
calc.divide(5, 0)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Pattern 2: Fixtures for Setup and Teardown
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
# test_database.py
|
|
69
|
+
import pytest
|
|
70
|
+
from typing import Generator
|
|
71
|
+
|
|
72
|
+
class Database:
|
|
73
|
+
"""Simple database class."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, connection_string: str):
|
|
76
|
+
self.connection_string = connection_string
|
|
77
|
+
self.connected = False
|
|
78
|
+
|
|
79
|
+
def connect(self):
|
|
80
|
+
"""Connect to database."""
|
|
81
|
+
self.connected = True
|
|
82
|
+
|
|
83
|
+
def disconnect(self):
|
|
84
|
+
"""Disconnect from database."""
|
|
85
|
+
self.connected = False
|
|
86
|
+
|
|
87
|
+
def query(self, sql: str) -> list:
|
|
88
|
+
"""Execute query."""
|
|
89
|
+
if not self.connected:
|
|
90
|
+
raise RuntimeError("Not connected")
|
|
91
|
+
return [{"id": 1, "name": "Test"}]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pytest.fixture
|
|
95
|
+
def db() -> Generator[Database, None, None]:
|
|
96
|
+
"""Fixture that provides connected database."""
|
|
97
|
+
# Setup
|
|
98
|
+
database = Database("sqlite:///:memory:")
|
|
99
|
+
database.connect()
|
|
100
|
+
|
|
101
|
+
# Provide to test
|
|
102
|
+
yield database
|
|
103
|
+
|
|
104
|
+
# Teardown
|
|
105
|
+
database.disconnect()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_database_query(db):
|
|
109
|
+
"""Test database query with fixture."""
|
|
110
|
+
results = db.query("SELECT * FROM users")
|
|
111
|
+
assert len(results) == 1
|
|
112
|
+
assert results[0]["name"] == "Test"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@pytest.fixture(scope="session")
|
|
116
|
+
def app_config():
|
|
117
|
+
"""Session-scoped fixture - created once per test session."""
|
|
118
|
+
return {
|
|
119
|
+
"database_url": "postgresql://localhost/test",
|
|
120
|
+
"api_key": "test-key",
|
|
121
|
+
"debug": True
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.fixture(scope="module")
|
|
126
|
+
def api_client(app_config):
|
|
127
|
+
"""Module-scoped fixture - created once per test module."""
|
|
128
|
+
# Setup expensive resource
|
|
129
|
+
client = {"config": app_config, "session": "active"}
|
|
130
|
+
yield client
|
|
131
|
+
# Cleanup
|
|
132
|
+
client["session"] = "closed"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_api_client(api_client):
|
|
136
|
+
"""Test using api client fixture."""
|
|
137
|
+
assert api_client["session"] == "active"
|
|
138
|
+
assert api_client["config"]["debug"] is True
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Pattern 3: Parameterized Tests
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
# test_validation.py
|
|
145
|
+
import pytest
|
|
146
|
+
|
|
147
|
+
def is_valid_email(email: str) -> bool:
|
|
148
|
+
"""Check if email is valid."""
|
|
149
|
+
return "@" in email and "." in email.split("@")[1]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@pytest.mark.parametrize("email,expected", [
|
|
153
|
+
("user@example.com", True),
|
|
154
|
+
("test.user@domain.co.uk", True),
|
|
155
|
+
("invalid.email", False),
|
|
156
|
+
("@example.com", False),
|
|
157
|
+
("user@domain", False),
|
|
158
|
+
("", False),
|
|
159
|
+
])
|
|
160
|
+
def test_email_validation(email, expected):
|
|
161
|
+
"""Test email validation with various inputs."""
|
|
162
|
+
assert is_valid_email(email) == expected
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@pytest.mark.parametrize("a,b,expected", [
|
|
166
|
+
(2, 3, 5),
|
|
167
|
+
(0, 0, 0),
|
|
168
|
+
(-1, 1, 0),
|
|
169
|
+
(100, 200, 300),
|
|
170
|
+
(-5, -5, -10),
|
|
171
|
+
])
|
|
172
|
+
def test_addition_parameterized(a, b, expected):
|
|
173
|
+
"""Test addition with multiple parameter sets."""
|
|
174
|
+
from test_calculator import Calculator
|
|
175
|
+
calc = Calculator()
|
|
176
|
+
assert calc.add(a, b) == expected
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Using pytest.param for special cases
|
|
180
|
+
@pytest.mark.parametrize("value,expected", [
|
|
181
|
+
pytest.param(1, True, id="positive"),
|
|
182
|
+
pytest.param(0, False, id="zero"),
|
|
183
|
+
pytest.param(-1, False, id="negative"),
|
|
184
|
+
])
|
|
185
|
+
def test_is_positive(value, expected):
|
|
186
|
+
"""Test with custom test IDs."""
|
|
187
|
+
assert (value > 0) == expected
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Pattern 4: Mocking with unittest.mock
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
# test_api_client.py
|
|
194
|
+
import pytest
|
|
195
|
+
from unittest.mock import Mock, patch, MagicMock
|
|
196
|
+
import requests
|
|
197
|
+
|
|
198
|
+
class APIClient:
|
|
199
|
+
"""Simple API client."""
|
|
200
|
+
|
|
201
|
+
def __init__(self, base_url: str):
|
|
202
|
+
self.base_url = base_url
|
|
203
|
+
|
|
204
|
+
def get_user(self, user_id: int) -> dict:
|
|
205
|
+
"""Fetch user from API."""
|
|
206
|
+
response = requests.get(f"{self.base_url}/users/{user_id}")
|
|
207
|
+
response.raise_for_status()
|
|
208
|
+
return response.json()
|
|
209
|
+
|
|
210
|
+
def create_user(self, data: dict) -> dict:
|
|
211
|
+
"""Create new user."""
|
|
212
|
+
response = requests.post(f"{self.base_url}/users", json=data)
|
|
213
|
+
response.raise_for_status()
|
|
214
|
+
return response.json()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_get_user_success():
|
|
218
|
+
"""Test successful API call with mock."""
|
|
219
|
+
client = APIClient("https://api.example.com")
|
|
220
|
+
|
|
221
|
+
mock_response = Mock()
|
|
222
|
+
mock_response.json.return_value = {"id": 1, "name": "John Doe"}
|
|
223
|
+
mock_response.raise_for_status.return_value = None
|
|
224
|
+
|
|
225
|
+
with patch("requests.get", return_value=mock_response) as mock_get:
|
|
226
|
+
user = client.get_user(1)
|
|
227
|
+
|
|
228
|
+
assert user["id"] == 1
|
|
229
|
+
assert user["name"] == "John Doe"
|
|
230
|
+
mock_get.assert_called_once_with("https://api.example.com/users/1")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_get_user_not_found():
|
|
234
|
+
"""Test API call with 404 error."""
|
|
235
|
+
client = APIClient("https://api.example.com")
|
|
236
|
+
|
|
237
|
+
mock_response = Mock()
|
|
238
|
+
mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found")
|
|
239
|
+
|
|
240
|
+
with patch("requests.get", return_value=mock_response):
|
|
241
|
+
with pytest.raises(requests.HTTPError):
|
|
242
|
+
client.get_user(999)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@patch("requests.post")
|
|
246
|
+
def test_create_user(mock_post):
|
|
247
|
+
"""Test user creation with decorator syntax."""
|
|
248
|
+
client = APIClient("https://api.example.com")
|
|
249
|
+
|
|
250
|
+
mock_post.return_value.json.return_value = {"id": 2, "name": "Jane Doe"}
|
|
251
|
+
mock_post.return_value.raise_for_status.return_value = None
|
|
252
|
+
|
|
253
|
+
user_data = {"name": "Jane Doe", "email": "jane@example.com"}
|
|
254
|
+
result = client.create_user(user_data)
|
|
255
|
+
|
|
256
|
+
assert result["id"] == 2
|
|
257
|
+
mock_post.assert_called_once()
|
|
258
|
+
call_args = mock_post.call_args
|
|
259
|
+
assert call_args.kwargs["json"] == user_data
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Pattern 5: Testing Exceptions
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
# test_exceptions.py
|
|
266
|
+
import pytest
|
|
267
|
+
|
|
268
|
+
def divide(a: float, b: float) -> float:
|
|
269
|
+
"""Divide a by b."""
|
|
270
|
+
if b == 0:
|
|
271
|
+
raise ZeroDivisionError("Division by zero")
|
|
272
|
+
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
|
|
273
|
+
raise TypeError("Arguments must be numbers")
|
|
274
|
+
return a / b
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_zero_division():
|
|
278
|
+
"""Test exception is raised for division by zero."""
|
|
279
|
+
with pytest.raises(ZeroDivisionError):
|
|
280
|
+
divide(10, 0)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_zero_division_with_message():
|
|
284
|
+
"""Test exception message."""
|
|
285
|
+
with pytest.raises(ZeroDivisionError, match="Division by zero"):
|
|
286
|
+
divide(5, 0)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_type_error():
|
|
290
|
+
"""Test type error exception."""
|
|
291
|
+
with pytest.raises(TypeError, match="must be numbers"):
|
|
292
|
+
divide("10", 5)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_exception_info():
|
|
296
|
+
"""Test accessing exception info."""
|
|
297
|
+
with pytest.raises(ValueError) as exc_info:
|
|
298
|
+
int("not a number")
|
|
299
|
+
|
|
300
|
+
assert "invalid literal" in str(exc_info.value)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
For advanced patterns including async testing, monkeypatching, temporary files, conftest setup, property-based testing, database testing, CI/CD integration, and configuration files, see [references/advanced-patterns.md](references/advanced-patterns.md)
|
|
304
|
+
|
|
305
|
+
## Test Design Principles
|
|
306
|
+
|
|
307
|
+
### One Behavior Per Test
|
|
308
|
+
|
|
309
|
+
Each test should verify exactly one behavior. This makes failures easy to diagnose and tests easy to maintain.
|
|
310
|
+
|
|
311
|
+
```python
|
|
312
|
+
# BAD - testing multiple behaviors
|
|
313
|
+
def test_user_service():
|
|
314
|
+
user = service.create_user(data)
|
|
315
|
+
assert user.id is not None
|
|
316
|
+
assert user.email == data["email"]
|
|
317
|
+
updated = service.update_user(user.id, {"name": "New"})
|
|
318
|
+
assert updated.name == "New"
|
|
319
|
+
|
|
320
|
+
# GOOD - focused tests
|
|
321
|
+
def test_create_user_assigns_id():
|
|
322
|
+
user = service.create_user(data)
|
|
323
|
+
assert user.id is not None
|
|
324
|
+
|
|
325
|
+
def test_create_user_stores_email():
|
|
326
|
+
user = service.create_user(data)
|
|
327
|
+
assert user.email == data["email"]
|
|
328
|
+
|
|
329
|
+
def test_update_user_changes_name():
|
|
330
|
+
user = service.create_user(data)
|
|
331
|
+
updated = service.update_user(user.id, {"name": "New"})
|
|
332
|
+
assert updated.name == "New"
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Test Error Paths
|
|
336
|
+
|
|
337
|
+
Always test failure cases, not just happy paths.
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
def test_get_user_raises_not_found():
|
|
341
|
+
with pytest.raises(UserNotFoundError) as exc_info:
|
|
342
|
+
service.get_user("nonexistent-id")
|
|
343
|
+
|
|
344
|
+
assert "nonexistent-id" in str(exc_info.value)
|
|
345
|
+
|
|
346
|
+
def test_create_user_rejects_invalid_email():
|
|
347
|
+
with pytest.raises(ValueError, match="Invalid email format"):
|
|
348
|
+
service.create_user({"email": "not-an-email"})
|
|
349
|
+
```
|