qa-skills 3.0.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/README.md +168 -0
- package/bin/cli.js +42 -0
- package/dist/agents/registry.d.ts +5 -0
- package/dist/agents/registry.d.ts.map +1 -0
- package/dist/agents/registry.js +101 -0
- package/dist/agents/registry.js.map +1 -0
- package/dist/agents/types.d.ts +9 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +2 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/dependencies.d.ts +21 -0
- package/dist/dependencies.d.ts.map +1 -0
- package/dist/dependencies.js +125 -0
- package/dist/dependencies.js.map +1 -0
- package/dist/installer.d.ts +25 -0
- package/dist/installer.d.ts.map +1 -0
- package/dist/installer.js +437 -0
- package/dist/installer.js.map +1 -0
- package/dist/scaffold.d.ts +27 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +182 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +40 -0
- package/skills/qa-accessibility-test-writer/SKILL.md +127 -0
- package/skills/qa-accessibility-test-writer/references/axe-core-patterns.md +349 -0
- package/skills/qa-accessibility-test-writer/references/best-practices.md +184 -0
- package/skills/qa-accessibility-test-writer/references/wcag-tests.md +331 -0
- package/skills/qa-api-contract-curator/SKILL.md +104 -0
- package/skills/qa-api-contract-curator/references/breaking-changes.md +363 -0
- package/skills/qa-api-contract-curator/references/openapi-structure.md +404 -0
- package/skills/qa-browser-data-collector/SKILL.md +132 -0
- package/skills/qa-browser-data-collector/references/data-collection-checklist.md +91 -0
- package/skills/qa-browser-data-collector/references/playwright-mcp-patterns.md +113 -0
- package/skills/qa-bug-ticket-creator/SKILL.md +148 -0
- package/skills/qa-bug-ticket-creator/references/bug-report-format.md +149 -0
- package/skills/qa-bug-ticket-creator/references/severity-guide.md +81 -0
- package/skills/qa-bug-ticket-creator/templates/bug-ticket-template.md +39 -0
- package/skills/qa-changelog-analyzer/SKILL.md +134 -0
- package/skills/qa-changelog-analyzer/references/git-analysis-patterns.md +138 -0
- package/skills/qa-changelog-analyzer/references/impact-mapping.md +120 -0
- package/skills/qa-clickup-integration/SKILL.md +166 -0
- package/skills/qa-clickup-integration/references/api-patterns.md +102 -0
- package/skills/qa-clickup-integration/references/field-mapping.md +71 -0
- package/skills/qa-codeceptjs-writer/SKILL.md +136 -0
- package/skills/qa-codeceptjs-writer/references/best-practices.md +207 -0
- package/skills/qa-codeceptjs-writer/references/config.md +255 -0
- package/skills/qa-codeceptjs-writer/references/patterns.md +285 -0
- package/skills/qa-coverage-analyzer/SKILL.md +166 -0
- package/skills/qa-coverage-analyzer/references/best-practices.md +142 -0
- package/skills/qa-coverage-analyzer/references/coverage-dimensions.md +155 -0
- package/skills/qa-coverage-analyzer/references/tools.md +204 -0
- package/skills/qa-cypress-writer/SKILL.md +134 -0
- package/skills/qa-cypress-writer/references/assertions.md +121 -0
- package/skills/qa-cypress-writer/references/best-practices.md +82 -0
- package/skills/qa-cypress-writer/references/config.md +121 -0
- package/skills/qa-cypress-writer/references/patterns.md +170 -0
- package/skills/qa-data-factory/SKILL.md +126 -0
- package/skills/qa-data-factory/references/factory-patterns.md +164 -0
- package/skills/qa-data-factory/references/faker-guide.md +131 -0
- package/skills/qa-diagram-generator/SKILL.md +125 -0
- package/skills/qa-diagram-generator/references/c4-model.md +53 -0
- package/skills/qa-diagram-generator/references/charts.md +58 -0
- package/skills/qa-diagram-generator/references/class-diagram.md +85 -0
- package/skills/qa-diagram-generator/references/er-diagram.md +69 -0
- package/skills/qa-diagram-generator/references/flowchart.md +92 -0
- package/skills/qa-diagram-generator/references/from-screenshot.md +45 -0
- package/skills/qa-diagram-generator/references/gantt.md +49 -0
- package/skills/qa-diagram-generator/references/journey.md +50 -0
- package/skills/qa-diagram-generator/references/mindmap.md +75 -0
- package/skills/qa-diagram-generator/references/sequence.md +69 -0
- package/skills/qa-diagram-generator/references/state-diagram.md +56 -0
- package/skills/qa-discovery-interview/SKILL.md +182 -0
- package/skills/qa-discovery-interview/references/completeness-checklist.md +53 -0
- package/skills/qa-discovery-interview/references/conflict-patterns.md +101 -0
- package/skills/qa-discovery-interview/references/qa-categories.md +147 -0
- package/skills/qa-discovery-interview/templates/qa-brief-template.md +168 -0
- package/skills/qa-environment-checker/SKILL.md +142 -0
- package/skills/qa-environment-checker/references/dependency-matrix.md +101 -0
- package/skills/qa-environment-checker/references/health-checks.md +209 -0
- package/skills/qa-environment-checker/templates/env-readiness-template.md +64 -0
- package/skills/qa-flaky-detector/SKILL.md +153 -0
- package/skills/qa-flaky-detector/references/ci-analysis.md +140 -0
- package/skills/qa-flaky-detector/references/flaky-patterns.md +247 -0
- package/skills/qa-github-issues-enhanced/SKILL.md +175 -0
- package/skills/qa-github-issues-enhanced/references/issue-templates.md +425 -0
- package/skills/qa-github-issues-enhanced/references/label-taxonomy.md +130 -0
- package/skills/qa-github-issues-enhanced/references/workflow-patterns.md +188 -0
- package/skills/qa-httpx-writer/SKILL.md +138 -0
- package/skills/qa-httpx-writer/references/assertions.md +195 -0
- package/skills/qa-httpx-writer/references/best-practices.md +140 -0
- package/skills/qa-httpx-writer/references/config.md +212 -0
- package/skills/qa-httpx-writer/references/patterns.md +262 -0
- package/skills/qa-jest-writer/SKILL.md +131 -0
- package/skills/qa-jest-writer/references/assertions.md +125 -0
- package/skills/qa-jest-writer/references/best-practices.md +136 -0
- package/skills/qa-jest-writer/references/config.md +134 -0
- package/skills/qa-jest-writer/references/patterns.md +172 -0
- package/skills/qa-jira-integration/SKILL.md +135 -0
- package/skills/qa-jira-integration/references/api-patterns.md +143 -0
- package/skills/qa-jira-integration/references/field-mapping.md +79 -0
- package/skills/qa-jira-integration/references/xray-integration.md +85 -0
- package/skills/qa-jmeter-writer/SKILL.md +171 -0
- package/skills/qa-jmeter-writer/references/best-practices.md +157 -0
- package/skills/qa-jmeter-writer/references/config.md +204 -0
- package/skills/qa-jmeter-writer/references/patterns.md +242 -0
- package/skills/qa-junit5-writer/SKILL.md +157 -0
- package/skills/qa-junit5-writer/references/assertions.md +118 -0
- package/skills/qa-junit5-writer/references/config.md +97 -0
- package/skills/qa-junit5-writer/references/patterns.md +162 -0
- package/skills/qa-k6-writer/SKILL.md +155 -0
- package/skills/qa-k6-writer/references/best-practices.md +236 -0
- package/skills/qa-k6-writer/references/config.md +219 -0
- package/skills/qa-k6-writer/references/patterns.md +304 -0
- package/skills/qa-linear-integration/SKILL.md +137 -0
- package/skills/qa-linear-integration/references/api-patterns.md +249 -0
- package/skills/qa-linear-integration/references/field-mapping.md +121 -0
- package/skills/qa-locust-writer/SKILL.md +151 -0
- package/skills/qa-locust-writer/references/best-practices.md +126 -0
- package/skills/qa-locust-writer/references/config.md +170 -0
- package/skills/qa-locust-writer/references/patterns.md +235 -0
- package/skills/qa-manual-test-designer/SKILL.md +145 -0
- package/skills/qa-manual-test-designer/references/exploratory-charters.md +138 -0
- package/skills/qa-manual-test-designer/references/personas.md +146 -0
- package/skills/qa-manual-test-designer/templates/exploratory-charter-template.md +47 -0
- package/skills/qa-manual-test-designer/templates/test-case-template.md +31 -0
- package/skills/qa-mobile-test-writer/SKILL.md +144 -0
- package/skills/qa-mobile-test-writer/references/best-practices.md +214 -0
- package/skills/qa-mobile-test-writer/references/config.md +309 -0
- package/skills/qa-mobile-test-writer/references/patterns.md +304 -0
- package/skills/qa-nfr-analyst/SKILL.md +177 -0
- package/skills/qa-nfr-analyst/references/iso-25010-model.md +159 -0
- package/skills/qa-nfr-analyst/references/owasp-wstg-baseline.md +202 -0
- package/skills/qa-nfr-analyst/references/wcag-checklist.md +184 -0
- package/skills/qa-nfr-analyst/templates/owasp-checklist-template.md +89 -0
- package/skills/qa-nfr-analyst/templates/wcag-checklist-template.md +48 -0
- package/skills/qa-orchestrator/SKILL.md +132 -0
- package/skills/qa-orchestrator/references/handoff-chains.md +105 -0
- package/skills/qa-orchestrator/references/pipeline-modes.md +115 -0
- package/skills/qa-orchestrator/references/scheduler-rules.md +84 -0
- package/skills/qa-pact-writer/SKILL.md +133 -0
- package/skills/qa-pact-writer/references/best-practices.md +100 -0
- package/skills/qa-pact-writer/references/config.md +135 -0
- package/skills/qa-pact-writer/references/patterns.md +161 -0
- package/skills/qa-plan-creator/SKILL.md +139 -0
- package/skills/qa-plan-creator/references/introduction-plan.md +43 -0
- package/skills/qa-plan-creator/references/migration-plan.md +44 -0
- package/skills/qa-plan-creator/references/onboarding-plan.md +46 -0
- package/skills/qa-plan-creator/references/performance-plan.md +44 -0
- package/skills/qa-plan-creator/references/regression-plan.md +45 -0
- package/skills/qa-plan-creator/references/release-plan.md +45 -0
- package/skills/qa-plan-creator/references/sprint-plan.md +44 -0
- package/skills/qa-plan-creator/references/test-plan.md +59 -0
- package/skills/qa-plan-creator/references/uat-plan.md +43 -0
- package/skills/qa-plan-creator/templates/checklist-template.md +36 -0
- package/skills/qa-plan-creator/templates/regression-checklist-template.md +49 -0
- package/skills/qa-plan-creator/templates/release-checklist-template.md +46 -0
- package/skills/qa-plan-creator/templates/test-plan-template.md +74 -0
- package/skills/qa-playwright-py-writer/SKILL.md +156 -0
- package/skills/qa-playwright-py-writer/references/best-practices.md +194 -0
- package/skills/qa-playwright-py-writer/references/config.md +195 -0
- package/skills/qa-playwright-py-writer/references/patterns.md +212 -0
- package/skills/qa-playwright-ts-writer/SKILL.md +151 -0
- package/skills/qa-playwright-ts-writer/references/assertions.md +109 -0
- package/skills/qa-playwright-ts-writer/references/best-practices.md +191 -0
- package/skills/qa-playwright-ts-writer/references/config.md +144 -0
- package/skills/qa-playwright-ts-writer/references/patterns.md +171 -0
- package/skills/qa-pytest-writer/SKILL.md +145 -0
- package/skills/qa-pytest-writer/references/assertions.md +149 -0
- package/skills/qa-pytest-writer/references/best-practices.md +97 -0
- package/skills/qa-pytest-writer/references/config.md +176 -0
- package/skills/qa-pytest-writer/references/patterns.md +251 -0
- package/skills/qa-qase-integration/SKILL.md +149 -0
- package/skills/qa-qase-integration/references/api-reference.md +354 -0
- package/skills/qa-qase-integration/references/ci-integration.md +196 -0
- package/skills/qa-qase-integration/references/field-mapping.md +157 -0
- package/skills/qa-requirements-generator/SKILL.md +152 -0
- package/skills/qa-requirements-generator/references/iso-29148-structure.md +153 -0
- package/skills/qa-requirements-generator/references/requirement-patterns.md +278 -0
- package/skills/qa-rest-assured-writer/SKILL.md +137 -0
- package/skills/qa-rest-assured-writer/references/best-practices.md +50 -0
- package/skills/qa-rest-assured-writer/references/config.md +124 -0
- package/skills/qa-rest-assured-writer/references/patterns.md +192 -0
- package/skills/qa-risk-analyzer/SKILL.md +158 -0
- package/skills/qa-risk-analyzer/references/impact-analysis.md +133 -0
- package/skills/qa-risk-analyzer/references/risk-factors.md +123 -0
- package/skills/qa-robot-framework-writer/SKILL.md +147 -0
- package/skills/qa-robot-framework-writer/references/best-practices.md +249 -0
- package/skills/qa-robot-framework-writer/references/config.md +204 -0
- package/skills/qa-robot-framework-writer/references/libraries.md +273 -0
- package/skills/qa-robot-framework-writer/references/patterns.md +216 -0
- package/skills/qa-security-test-writer/SKILL.md +123 -0
- package/skills/qa-security-test-writer/references/best-practices.md +155 -0
- package/skills/qa-security-test-writer/references/owasp-top10.md +331 -0
- package/skills/qa-security-test-writer/references/zap-config.md +258 -0
- package/skills/qa-selenium-java-writer/SKILL.md +143 -0
- package/skills/qa-selenium-java-writer/references/best-practices.md +59 -0
- package/skills/qa-selenium-java-writer/references/config.md +143 -0
- package/skills/qa-selenium-java-writer/references/patterns.md +170 -0
- package/skills/qa-selenium-py-writer/SKILL.md +150 -0
- package/skills/qa-selenium-py-writer/references/best-practices.md +175 -0
- package/skills/qa-selenium-py-writer/references/config.md +224 -0
- package/skills/qa-selenium-py-writer/references/patterns.md +255 -0
- package/skills/qa-shortcut-integration/SKILL.md +143 -0
- package/skills/qa-shortcut-integration/references/api-patterns.md +126 -0
- package/skills/qa-shortcut-integration/references/field-mapping.md +66 -0
- package/skills/qa-spec-auditor/SKILL.md +162 -0
- package/skills/qa-spec-auditor/references/audit-checklist.md +144 -0
- package/skills/qa-spec-auditor/references/drift-patterns.md +207 -0
- package/skills/qa-spec-writer/SKILL.md +143 -0
- package/skills/qa-spec-writer/references/gherkin-guide.md +253 -0
- package/skills/qa-spec-writer/references/specification-patterns.md +274 -0
- package/skills/qa-spring-test-writer/SKILL.md +170 -0
- package/skills/qa-spring-test-writer/references/best-practices.md +57 -0
- package/skills/qa-spring-test-writer/references/config.md +179 -0
- package/skills/qa-spring-test-writer/references/patterns.md +235 -0
- package/skills/qa-supertest-writer/SKILL.md +150 -0
- package/skills/qa-supertest-writer/references/assertions.md +192 -0
- package/skills/qa-supertest-writer/references/best-practices.md +102 -0
- package/skills/qa-supertest-writer/references/config.md +166 -0
- package/skills/qa-supertest-writer/references/patterns.md +242 -0
- package/skills/qa-task-creator/SKILL.md +142 -0
- package/skills/qa-task-creator/references/linking-patterns.md +127 -0
- package/skills/qa-task-creator/references/task-types.md +169 -0
- package/skills/qa-task-creator/templates/task-template.md +24 -0
- package/skills/qa-test-doc-compiler/SKILL.md +114 -0
- package/skills/qa-test-doc-compiler/references/agile-tailoring.md +220 -0
- package/skills/qa-test-doc-compiler/references/iso-29119-3-documents.md +302 -0
- package/skills/qa-test-healer/SKILL.md +101 -0
- package/skills/qa-test-healer/references/diagnosis-patterns.md +142 -0
- package/skills/qa-test-healer/references/fix-strategies.md +177 -0
- package/skills/qa-test-reporter/SKILL.md +130 -0
- package/skills/qa-test-reporter/references/best-practices.md +162 -0
- package/skills/qa-test-reporter/references/iso-29119-reports.md +236 -0
- package/skills/qa-test-reporter/references/report-formats.md +287 -0
- package/skills/qa-test-reviewer/SKILL.md +142 -0
- package/skills/qa-test-reviewer/references/anti-patterns.md +268 -0
- package/skills/qa-test-reviewer/references/review-checklist.md +93 -0
- package/skills/qa-test-strategy/SKILL.md +133 -0
- package/skills/qa-test-strategy/references/entry-exit-criteria.md +176 -0
- package/skills/qa-test-strategy/references/risk-matrix.md +102 -0
- package/skills/qa-test-strategy/references/testing-types.md +143 -0
- package/skills/qa-testcase-from-docs/SKILL.md +161 -0
- package/skills/qa-testcase-from-docs/references/test-case-format.md +196 -0
- package/skills/qa-testcase-from-docs/references/test-design-techniques.md +126 -0
- package/skills/qa-testcase-from-docs/templates/test-case-template.md +31 -0
- package/skills/qa-testcase-from-ui/SKILL.md +109 -0
- package/skills/qa-testcase-from-ui/references/ui-element-patterns.md +126 -0
- package/skills/qa-testcase-from-ui/references/visual-analysis-guide.md +146 -0
- package/skills/qa-testcase-from-ui/templates/test-case-template.md +31 -0
- package/skills/qa-visual-regression-writer/SKILL.md +175 -0
- package/skills/qa-visual-regression-writer/references/best-practices.md +154 -0
- package/skills/qa-visual-regression-writer/references/config.md +220 -0
- package/skills/qa-visual-regression-writer/references/patterns.md +213 -0
- package/skills/qa-vitest-writer/SKILL.md +141 -0
- package/skills/qa-vitest-writer/references/assertions.md +105 -0
- package/skills/qa-vitest-writer/references/best-practices.md +62 -0
- package/skills/qa-vitest-writer/references/config.md +127 -0
- package/skills/qa-vitest-writer/references/patterns.md +141 -0
- package/skills/qa-webdriverio-writer/SKILL.md +145 -0
- package/skills/qa-webdriverio-writer/references/best-practices.md +176 -0
- package/skills/qa-webdriverio-writer/references/config.md +240 -0
- package/skills/qa-webdriverio-writer/references/patterns.md +269 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Python API Testing Best Practices
|
|
2
|
+
|
|
3
|
+
## Async vs Sync
|
|
4
|
+
|
|
5
|
+
| Use Case | Recommendation |
|
|
6
|
+
| -------- | -------------- |
|
|
7
|
+
| **Most API tests** | Sync `httpx.Client` — simpler, sufficient for typical CRUD |
|
|
8
|
+
| **High concurrency** | Async `httpx.AsyncClient` — when testing many parallel requests |
|
|
9
|
+
| **Async app under test** | Async client — matches async handlers |
|
|
10
|
+
| **Mixed codebase** | Prefer sync unless async is required |
|
|
11
|
+
|
|
12
|
+
### When to Use Async
|
|
13
|
+
|
|
14
|
+
- Testing endpoints that benefit from concurrent requests
|
|
15
|
+
- Matching async application architecture
|
|
16
|
+
- Performance benchmarks with many simultaneous calls
|
|
17
|
+
|
|
18
|
+
### Sync Simplicity
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
# Prefer sync for straightforward tests
|
|
22
|
+
def test_get_users(client, base_url):
|
|
23
|
+
response = client.get(f"{base_url}/users")
|
|
24
|
+
assert response.status_code == 200
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Session Reuse
|
|
28
|
+
|
|
29
|
+
| Scope | Pros | Cons |
|
|
30
|
+
| ----- | ---- | ---- |
|
|
31
|
+
| **Function** | Isolated; no shared state | New connection per test |
|
|
32
|
+
| **Module** | Fewer connections | Shared state across tests in module |
|
|
33
|
+
| **Session** | Fastest; single connection pool | Must avoid mutating shared state |
|
|
34
|
+
|
|
35
|
+
**Recommendation:** Use function-scoped client by default. Use session scope only when tests are read-only and do not mutate server state.
|
|
36
|
+
|
|
37
|
+
## Schema Validation
|
|
38
|
+
|
|
39
|
+
1. **Use Pydantic when** — OpenAPI contract exists; response shape is well-defined
|
|
40
|
+
2. **Use JSON Schema when** — Contract is in JSON Schema format; no Pydantic in project
|
|
41
|
+
3. **Use manual assertions when** — Only a few fields matter; full schema is overkill
|
|
42
|
+
|
|
43
|
+
### Contract-Driven
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# From qa-api-contract-curator OpenAPI spec
|
|
47
|
+
class UserResponse(BaseModel):
|
|
48
|
+
id: int
|
|
49
|
+
email: EmailStr
|
|
50
|
+
name: str | None = None
|
|
51
|
+
|
|
52
|
+
def test_user_matches_contract(client, base_url, auth_headers):
|
|
53
|
+
response = client.get(f"{base_url}/users/1", headers=auth_headers)
|
|
54
|
+
UserResponse.model_validate(response.json()) # Fails if contract violated
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Test Isolation
|
|
58
|
+
|
|
59
|
+
1. **Independent tests** — Each test passes regardless of order
|
|
60
|
+
2. **No shared mutable state** — Avoid global variables; use fixtures
|
|
61
|
+
3. **Clean data** — Create/delete test data per test or use unique identifiers
|
|
62
|
+
4. **Idempotent when possible** — GET requests are safe; POST may create duplicates — use unique emails, UUIDs
|
|
63
|
+
|
|
64
|
+
### Unique Test Data
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import uuid
|
|
68
|
+
|
|
69
|
+
def test_create_user_unique(client, base_url, auth_headers):
|
|
70
|
+
email = f"test-{uuid.uuid4().hex}@example.com"
|
|
71
|
+
response = client.post(
|
|
72
|
+
f"{base_url}/users",
|
|
73
|
+
json={"email": email, "name": "Test"},
|
|
74
|
+
headers=auth_headers
|
|
75
|
+
)
|
|
76
|
+
assert response.status_code == 201
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Error Scenarios
|
|
80
|
+
|
|
81
|
+
Cover at least:
|
|
82
|
+
|
|
83
|
+
| Scenario | Status | Assertion |
|
|
84
|
+
| -------- | ------ | --------- |
|
|
85
|
+
| Success | 200/201/204 | Per contract |
|
|
86
|
+
| Validation error | 400 | Error payload structure |
|
|
87
|
+
| Unauthorized | 401 | When missing/invalid auth |
|
|
88
|
+
| Forbidden | 403 | Insufficient permissions |
|
|
89
|
+
| Not found | 404 | Missing resource |
|
|
90
|
+
| Conflict | 409 | Duplicate, constraint violation |
|
|
91
|
+
|
|
92
|
+
## Avoid
|
|
93
|
+
|
|
94
|
+
1. **Hardcoded secrets** — Use `os.environ` or pytest fixtures
|
|
95
|
+
2. **Fragile assertions** — Prefer partial match for dynamic fields (id, createdAt)
|
|
96
|
+
3. **Sleep/timeout for flakiness** — Use retries or deterministic data instead
|
|
97
|
+
4. **Testing implementation** — Test behavior and contract, not internal routes
|
|
98
|
+
5. **Large fixtures** — Minimize payload size; use factories for scale
|
|
99
|
+
6. **External services in unit tests** — Mock third-party APIs; use real API only in integration tests
|
|
100
|
+
|
|
101
|
+
## File Organization
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
tests/
|
|
105
|
+
conftest.py # base_url, client, auth
|
|
106
|
+
api/
|
|
107
|
+
conftest.py # auth_headers, token
|
|
108
|
+
test_users_api.py
|
|
109
|
+
test_products_api.py
|
|
110
|
+
unit/ # Mocked API calls
|
|
111
|
+
test_services.py
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Parallel Execution
|
|
115
|
+
|
|
116
|
+
When using `pytest-xdist`:
|
|
117
|
+
|
|
118
|
+
- Use unique data (UUIDs, timestamps) to avoid collisions
|
|
119
|
+
- Or run API tests sequentially: `pytest -m api --forked` or single worker
|
|
120
|
+
- Consider separate DB/schema per worker for integration tests
|
|
121
|
+
|
|
122
|
+
## Dependencies
|
|
123
|
+
|
|
124
|
+
```toml
|
|
125
|
+
# pyproject.toml
|
|
126
|
+
[project.optional-dependencies]
|
|
127
|
+
test = [
|
|
128
|
+
"httpx>=0.24.0",
|
|
129
|
+
"pytest>=7.0",
|
|
130
|
+
"pytest-asyncio>=0.21.0",
|
|
131
|
+
"pydantic>=2.0",
|
|
132
|
+
]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Contract-Driven Workflow
|
|
136
|
+
|
|
137
|
+
1. **Get OpenAPI** — From qa-api-contract-curator
|
|
138
|
+
2. **Generate Pydantic models** — From `components.schemas` (or use datamodel-code-generator)
|
|
139
|
+
3. **Generate tests** — One test per operation × status code
|
|
140
|
+
4. **Validate responses** — `Model.model_validate(response.json())`
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# httpx Test Configuration
|
|
2
|
+
|
|
3
|
+
## conftest.py Structure
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
# tests/conftest.py
|
|
7
|
+
import os
|
|
8
|
+
import pytest
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def base_url():
|
|
14
|
+
return os.environ.get("API_BASE_URL", "http://localhost:8000")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def client(base_url):
|
|
19
|
+
"""Sync client; use for non-async tests."""
|
|
20
|
+
with httpx.Client(
|
|
21
|
+
base_url=base_url,
|
|
22
|
+
timeout=30.0,
|
|
23
|
+
follow_redirects=True,
|
|
24
|
+
) as c:
|
|
25
|
+
yield c
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
async def async_client(base_url):
|
|
30
|
+
"""Async client; use with @pytest.mark.asyncio."""
|
|
31
|
+
async with httpx.AsyncClient(
|
|
32
|
+
base_url=base_url,
|
|
33
|
+
timeout=30.0,
|
|
34
|
+
follow_redirects=True,
|
|
35
|
+
) as c:
|
|
36
|
+
yield c
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Base URL Management
|
|
40
|
+
|
|
41
|
+
### Environment Variable
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# .env.test or pytest.ini
|
|
45
|
+
# API_BASE_URL=https://api.staging.example.com
|
|
46
|
+
|
|
47
|
+
@pytest.fixture
|
|
48
|
+
def base_url():
|
|
49
|
+
url = os.environ.get("API_BASE_URL")
|
|
50
|
+
if not url:
|
|
51
|
+
pytest.skip("API_BASE_URL not set")
|
|
52
|
+
return url
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Per-Environment
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
@pytest.fixture
|
|
59
|
+
def base_url():
|
|
60
|
+
env = os.environ.get("TEST_ENV", "local")
|
|
61
|
+
urls = {
|
|
62
|
+
"local": "http://localhost:8000",
|
|
63
|
+
"staging": "https://api.staging.example.com",
|
|
64
|
+
"integration": "https://api.integration.example.com",
|
|
65
|
+
}
|
|
66
|
+
return urls.get(env, urls["local"])
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Client Fixtures
|
|
70
|
+
|
|
71
|
+
### Function-Scoped (Default)
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
@pytest.fixture
|
|
75
|
+
def client(base_url):
|
|
76
|
+
with httpx.Client(base_url=base_url) as c:
|
|
77
|
+
yield c
|
|
78
|
+
# New client per test; no shared state
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Session-Scoped (Connection Reuse)
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
@pytest.fixture(scope="session")
|
|
85
|
+
def client(base_url):
|
|
86
|
+
with httpx.Client(base_url=base_url) as c:
|
|
87
|
+
yield c
|
|
88
|
+
# Reuse connection across tests; faster but shared state
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### With Custom Headers
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
@pytest.fixture
|
|
95
|
+
def client(base_url, default_headers):
|
|
96
|
+
with httpx.Client(
|
|
97
|
+
base_url=base_url,
|
|
98
|
+
headers=default_headers,
|
|
99
|
+
) as c:
|
|
100
|
+
yield c
|
|
101
|
+
|
|
102
|
+
@pytest.fixture
|
|
103
|
+
def default_headers():
|
|
104
|
+
return {
|
|
105
|
+
"Accept": "application/json",
|
|
106
|
+
"Content-Type": "application/json",
|
|
107
|
+
"User-Agent": "qa-httpx-tests/1.0",
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Auth Fixtures
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
@pytest.fixture
|
|
115
|
+
def token(client, base_url):
|
|
116
|
+
response = client.post(
|
|
117
|
+
f"{base_url}/auth/login",
|
|
118
|
+
json={
|
|
119
|
+
"email": os.environ.get("TEST_USER_EMAIL", "test@example.com"),
|
|
120
|
+
"password": os.environ.get("TEST_USER_PASSWORD", "test"),
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
assert response.status_code == 200
|
|
124
|
+
return response.json()["token"]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@pytest.fixture
|
|
128
|
+
def auth_headers(token):
|
|
129
|
+
return {"Authorization": f"Bearer {token}"}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@pytest.fixture
|
|
133
|
+
def auth_client(client, auth_headers):
|
|
134
|
+
client.headers.update(auth_headers)
|
|
135
|
+
return client
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## pytest-asyncio Configuration
|
|
139
|
+
|
|
140
|
+
### pyproject.toml
|
|
141
|
+
|
|
142
|
+
```toml
|
|
143
|
+
[tool.pytest.ini_options]
|
|
144
|
+
asyncio_mode = "auto"
|
|
145
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
146
|
+
testpaths = ["tests"]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### pytest.ini
|
|
150
|
+
|
|
151
|
+
```ini
|
|
152
|
+
[pytest]
|
|
153
|
+
asyncio_mode = auto
|
|
154
|
+
asyncio_default_fixture_loop_scope = function
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Timeout and Retry
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from httpx import Timeout, Limits
|
|
161
|
+
|
|
162
|
+
@pytest.fixture
|
|
163
|
+
def client(base_url):
|
|
164
|
+
with httpx.Client(
|
|
165
|
+
base_url=base_url,
|
|
166
|
+
timeout=Timeout(60.0),
|
|
167
|
+
limits=Limits(max_keepalive_connections=5, max_connections=10),
|
|
168
|
+
) as c:
|
|
169
|
+
yield c
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Markers for API Tests
|
|
173
|
+
|
|
174
|
+
```toml
|
|
175
|
+
# pyproject.toml
|
|
176
|
+
[tool.pytest.ini_options]
|
|
177
|
+
markers = [
|
|
178
|
+
"api: marks tests as API integration tests",
|
|
179
|
+
"slow: marks tests as slow (network-dependent)",
|
|
180
|
+
]
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
@pytest.mark.api
|
|
185
|
+
def test_users_endpoint(client, base_url):
|
|
186
|
+
response = client.get(f"{base_url}/users")
|
|
187
|
+
assert response.status_code == 200
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Directory Layout
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
tests/
|
|
194
|
+
conftest.py # Shared fixtures
|
|
195
|
+
api/
|
|
196
|
+
conftest.py # API-specific fixtures (auth, etc.)
|
|
197
|
+
test_users_api.py
|
|
198
|
+
test_products_api.py
|
|
199
|
+
test_auth_api.py
|
|
200
|
+
fixtures/
|
|
201
|
+
sample_users.json
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Environment Config
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
# tests/conftest.py
|
|
208
|
+
def pytest_configure(config):
|
|
209
|
+
os.environ.setdefault("TESTING", "1")
|
|
210
|
+
if "API_BASE_URL" not in os.environ:
|
|
211
|
+
os.environ["API_BASE_URL"] = "http://localhost:8000"
|
|
212
|
+
```
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# httpx API Test Patterns
|
|
2
|
+
|
|
3
|
+
## Sync Client (httpx.Client)
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
def test_get_users():
|
|
9
|
+
with httpx.Client(base_url="https://api.example.com") as client:
|
|
10
|
+
response = client.get("/users")
|
|
11
|
+
assert response.status_code == 200
|
|
12
|
+
data = response.json()
|
|
13
|
+
assert "users" in data
|
|
14
|
+
assert isinstance(data["users"], list)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Async Client (httpx.AsyncClient)
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import httpx
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
@pytest.mark.asyncio
|
|
24
|
+
async def test_get_users_async():
|
|
25
|
+
async with httpx.AsyncClient(base_url="https://api.example.com") as client:
|
|
26
|
+
response = await client.get("/users")
|
|
27
|
+
assert response.status_code == 200
|
|
28
|
+
data = response.json()
|
|
29
|
+
assert "users" in data
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## CRUD Endpoints
|
|
33
|
+
|
|
34
|
+
### GET (List)
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
def test_get_users_list(client, base_url):
|
|
38
|
+
response = client.get(f"{base_url}/users")
|
|
39
|
+
assert response.status_code == 200
|
|
40
|
+
assert "application/json" in response.headers.get("content-type", "")
|
|
41
|
+
data = response.json()
|
|
42
|
+
assert isinstance(data.get("users"), list)
|
|
43
|
+
|
|
44
|
+
def test_get_users_unauthorized(client, base_url):
|
|
45
|
+
response = client.get(f"{base_url}/users") # No auth
|
|
46
|
+
assert response.status_code == 401
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### GET (Single)
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
def test_get_user_by_id(client, base_url, auth_headers):
|
|
53
|
+
response = client.get(f"{base_url}/users/1", headers=auth_headers)
|
|
54
|
+
assert response.status_code == 200
|
|
55
|
+
user = response.json()
|
|
56
|
+
assert user["id"] == 1
|
|
57
|
+
assert "email" in user
|
|
58
|
+
|
|
59
|
+
def test_get_user_not_found(client, base_url, auth_headers):
|
|
60
|
+
response = client.get(f"{base_url}/users/99999", headers=auth_headers)
|
|
61
|
+
assert response.status_code == 404
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### POST (Create)
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
def test_create_user(client, base_url, auth_headers):
|
|
68
|
+
payload = {"email": "test@example.com", "name": "Test User"}
|
|
69
|
+
response = client.post(f"{base_url}/users", json=payload, headers=auth_headers)
|
|
70
|
+
assert response.status_code == 201
|
|
71
|
+
user = response.json()
|
|
72
|
+
assert "id" in user
|
|
73
|
+
assert user["email"] == "test@example.com"
|
|
74
|
+
|
|
75
|
+
def test_create_user_validation_error(client, base_url, auth_headers):
|
|
76
|
+
response = client.post(
|
|
77
|
+
f"{base_url}/users",
|
|
78
|
+
json={"email": "invalid"},
|
|
79
|
+
headers=auth_headers
|
|
80
|
+
)
|
|
81
|
+
assert response.status_code == 400
|
|
82
|
+
errors = response.json()
|
|
83
|
+
assert "errors" in errors or "message" in errors
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### PUT/PATCH (Update)
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
def test_update_user(client, base_url, auth_headers):
|
|
90
|
+
response = client.put(
|
|
91
|
+
f"{base_url}/users/1",
|
|
92
|
+
json={"name": "Updated Name"},
|
|
93
|
+
headers=auth_headers
|
|
94
|
+
)
|
|
95
|
+
assert response.status_code == 200
|
|
96
|
+
user = response.json()
|
|
97
|
+
assert user["name"] == "Updated Name"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### DELETE
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
def test_delete_user(client, base_url, auth_headers):
|
|
104
|
+
response = client.delete(f"{base_url}/users/1", headers=auth_headers)
|
|
105
|
+
assert response.status_code == 204
|
|
106
|
+
assert response.content == b""
|
|
107
|
+
|
|
108
|
+
def test_delete_user_not_found(client, base_url, auth_headers):
|
|
109
|
+
response = client.delete(f"{base_url}/users/99999", headers=auth_headers)
|
|
110
|
+
assert response.status_code == 404
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Authentication
|
|
114
|
+
|
|
115
|
+
### Bearer Token
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
@pytest.fixture
|
|
119
|
+
def auth_headers(token):
|
|
120
|
+
return {"Authorization": f"Bearer {token}"}
|
|
121
|
+
|
|
122
|
+
@pytest.fixture
|
|
123
|
+
def token(client, base_url):
|
|
124
|
+
response = client.post(
|
|
125
|
+
f"{base_url}/auth/login",
|
|
126
|
+
json={"email": "test@example.com", "password": "secret"}
|
|
127
|
+
)
|
|
128
|
+
assert response.status_code == 200
|
|
129
|
+
return response.json()["token"]
|
|
130
|
+
|
|
131
|
+
def test_protected_route(client, base_url, auth_headers):
|
|
132
|
+
response = client.get(f"{base_url}/users", headers=auth_headers)
|
|
133
|
+
assert response.status_code == 200
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### API Key
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
@pytest.fixture
|
|
140
|
+
def api_key_headers():
|
|
141
|
+
return {"X-API-Key": os.environ.get("TEST_API_KEY", "test-key")}
|
|
142
|
+
|
|
143
|
+
def test_with_api_key(client, base_url, api_key_headers):
|
|
144
|
+
response = client.get(f"{base_url}/api/data", headers=api_key_headers)
|
|
145
|
+
assert response.status_code == 200
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Basic Auth
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
def test_basic_auth(client, base_url):
|
|
152
|
+
auth = httpx.BasicAuth("user", "password")
|
|
153
|
+
response = client.get(f"{base_url}/admin", auth=auth)
|
|
154
|
+
assert response.status_code == 200
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### OAuth2
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
# Manual token flow
|
|
161
|
+
def test_oauth2_protected(client, base_url, oauth_token):
|
|
162
|
+
response = client.get(
|
|
163
|
+
f"{base_url}/protected",
|
|
164
|
+
headers={"Authorization": f"Bearer {oauth_token}"}
|
|
165
|
+
)
|
|
166
|
+
assert response.status_code == 200
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Cookies (Session)
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
def test_session_cookies(client, base_url):
|
|
173
|
+
# Login to set cookies
|
|
174
|
+
client.post(
|
|
175
|
+
f"{base_url}/auth/login",
|
|
176
|
+
json={"email": "test@example.com", "password": "secret"}
|
|
177
|
+
)
|
|
178
|
+
# Subsequent request uses session cookies
|
|
179
|
+
response = client.get(f"{base_url}/users")
|
|
180
|
+
assert response.status_code == 200
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## File Upload
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
def test_file_upload(client, base_url, auth_headers, tmp_path):
|
|
187
|
+
file_path = tmp_path / "sample.pdf"
|
|
188
|
+
file_path.write_bytes(b"%PDF-1.4 fake content")
|
|
189
|
+
|
|
190
|
+
with open(file_path, "rb") as f:
|
|
191
|
+
files = {"file": ("sample.pdf", f, "application/pdf")}
|
|
192
|
+
response = client.post(
|
|
193
|
+
f"{base_url}/upload",
|
|
194
|
+
files=files,
|
|
195
|
+
headers=auth_headers
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
assert response.status_code == 201
|
|
199
|
+
data = response.json()
|
|
200
|
+
assert "url" in data or "id" in data
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Streaming
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
def test_streaming_response(client, base_url):
|
|
207
|
+
with client.stream("GET", f"{base_url}/stream") as response:
|
|
208
|
+
assert response.status_code == 200
|
|
209
|
+
chunks = []
|
|
210
|
+
for chunk in response.iter_bytes():
|
|
211
|
+
chunks.append(chunk)
|
|
212
|
+
assert len(chunks) > 0
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Query Parameters
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
def test_query_params(client, base_url):
|
|
219
|
+
response = client.get(
|
|
220
|
+
f"{base_url}/search",
|
|
221
|
+
params={"q": "test", "limit": 5}
|
|
222
|
+
)
|
|
223
|
+
assert response.status_code == 200
|
|
224
|
+
# URL: /search?q=test&limit=5
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Retry Logic
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from httpx import Limits, Timeout
|
|
231
|
+
|
|
232
|
+
@pytest.fixture
|
|
233
|
+
def client_with_retry(base_url):
|
|
234
|
+
transport = httpx.HTTPTransport(retries=3)
|
|
235
|
+
with httpx.Client(
|
|
236
|
+
base_url=base_url,
|
|
237
|
+
transport=transport,
|
|
238
|
+
timeout=Timeout(30.0)
|
|
239
|
+
) as client:
|
|
240
|
+
yield client
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## requests Fallback
|
|
244
|
+
|
|
245
|
+
When httpx is not available, use requests with similar patterns:
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
import requests
|
|
249
|
+
|
|
250
|
+
def test_with_requests(base_url):
|
|
251
|
+
response = requests.get(f"{base_url}/users")
|
|
252
|
+
assert response.status_code == 200
|
|
253
|
+
data = response.json()
|
|
254
|
+
assert "users" in data
|
|
255
|
+
|
|
256
|
+
# Session for connection reuse
|
|
257
|
+
def test_with_session(base_url):
|
|
258
|
+
with requests.Session() as session:
|
|
259
|
+
session.headers.update({"Authorization": f"Bearer {token}"})
|
|
260
|
+
response = session.get(f"{base_url}/users")
|
|
261
|
+
assert response.status_code == 200
|
|
262
|
+
```
|