@sylix/coworker 2.0.11 → 2.0.12
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/dist/commands/slash/config.d.ts.map +1 -1
- package/dist/commands/slash/config.js +22 -4
- package/dist/commands/slash/config.js.map +1 -1
- package/dist/core/CoWorkerAgent.d.ts.map +1 -1
- package/dist/core/CoWorkerAgent.js +6 -3
- package/dist/core/CoWorkerAgent.js.map +1 -1
- package/dist/skills/defaults/accessibility/screen-reader-testing.md +545 -0
- package/dist/skills/defaults/accessibility/wcag-audit-patterns.md +555 -0
- package/dist/skills/defaults/ai-ml/rag.md +276 -0
- package/dist/skills/defaults/backend-development/api-design-principles.md +528 -0
- package/dist/skills/defaults/backend-development/api-design.md +285 -0
- package/dist/skills/defaults/backend-development/architecture-patterns.md +494 -0
- package/dist/skills/defaults/backend-development/async-python.md +237 -0
- package/dist/skills/defaults/backend-development/auth-implementation-patterns.md +638 -0
- package/dist/skills/defaults/backend-development/bazel-build-optimization.md +387 -0
- package/dist/skills/defaults/backend-development/billing-automation/SKILL.md +566 -0
- package/dist/skills/defaults/backend-development/code-review-excellence.md +538 -0
- package/dist/skills/defaults/backend-development/cqrs-implementation.md +554 -0
- package/dist/skills/defaults/backend-development/database-design.md +305 -0
- package/dist/skills/defaults/backend-development/debugging-strategies.md +536 -0
- package/dist/skills/defaults/backend-development/e2e-testing-patterns.md +544 -0
- package/dist/skills/defaults/backend-development/error-handling-patterns.md +641 -0
- package/dist/skills/defaults/backend-development/fastapi-templates.md +559 -0
- package/dist/skills/defaults/backend-development/fastapi.md +309 -0
- package/dist/skills/defaults/backend-development/git-advanced-workflows.md +405 -0
- package/dist/skills/defaults/backend-development/microservices-patterns.md +595 -0
- package/dist/skills/defaults/backend-development/microservices.md +284 -0
- package/dist/skills/defaults/backend-development/monorepo-management.md +623 -0
- package/dist/skills/defaults/backend-development/nodejs-backend-patterns.md +1048 -0
- package/dist/skills/defaults/backend-development/nx-workspace-patterns.md +457 -0
- package/dist/skills/defaults/backend-development/paypal-integration/SKILL.md +478 -0
- package/dist/skills/defaults/backend-development/pci-compliance/SKILL.md +480 -0
- package/dist/skills/defaults/backend-development/python-anti-patterns.md +349 -0
- package/dist/skills/defaults/backend-development/python-background-jobs.md +364 -0
- package/dist/skills/defaults/backend-development/python-code-style.md +360 -0
- package/dist/skills/defaults/backend-development/python-configuration.md +368 -0
- package/dist/skills/defaults/backend-development/python-design-patterns.md +296 -0
- package/dist/skills/defaults/backend-development/python-error-handling.md +323 -0
- package/dist/skills/defaults/backend-development/python-packaging.md +887 -0
- package/dist/skills/defaults/backend-development/python-performance-optimization.md +874 -0
- package/dist/skills/defaults/backend-development/python-project-structure.md +252 -0
- package/dist/skills/defaults/backend-development/python-resilience.md +376 -0
- package/dist/skills/defaults/backend-development/python-resource-management.md +421 -0
- package/dist/skills/defaults/backend-development/python-type-safety.md +428 -0
- package/dist/skills/defaults/backend-development/sql-optimization-patterns.md +509 -0
- package/dist/skills/defaults/backend-development/stripe-integration/SKILL.md +522 -0
- package/dist/skills/defaults/backend-development/turborepo-caching.md +376 -0
- package/dist/skills/defaults/blockchain/defi-protocol-templates.md +430 -0
- package/dist/skills/defaults/blockchain/nft-standards.md +364 -0
- package/dist/skills/defaults/blockchain/solidity-security.md +514 -0
- package/dist/skills/defaults/blockchain/web3-testing.md +360 -0
- package/dist/skills/defaults/business/competitive-landscape/SKILL.md +527 -0
- package/dist/skills/defaults/business/market-sizing-analysis/SKILL.md +451 -0
- package/dist/skills/defaults/business/startup-financial-modeling/SKILL.md +494 -0
- package/dist/skills/defaults/business/startup-metrics-framework/SKILL.md +564 -0
- package/dist/skills/defaults/business/team-composition-analysis.md +437 -0
- package/dist/skills/defaults/compliance/employment-contract-templates/SKILL.md +527 -0
- package/dist/skills/defaults/compliance/gdpr-data-handling/SKILL.md +630 -0
- package/dist/skills/defaults/data-engineering/airflow-dag-patterns.md +436 -0
- package/dist/skills/defaults/data-engineering/airflow.md +519 -0
- package/dist/skills/defaults/data-engineering/data-quality.md +583 -0
- package/dist/skills/defaults/data-engineering/dbt-transformation-patterns.md +482 -0
- package/dist/skills/defaults/data-engineering/dbt.md +556 -0
- package/dist/skills/defaults/data-engineering/ml-pipeline-workflow/SKILL.md +247 -0
- package/dist/skills/defaults/data-engineering/spark-optimization.md +348 -0
- package/dist/skills/defaults/data-engineering/spark.md +411 -0
- package/dist/skills/defaults/database/postgresql.md +202 -0
- package/dist/skills/defaults/debugging/systematic-debugging.md +249 -0
- package/dist/skills/defaults/devops/architecture-decision-records.md +448 -0
- package/dist/skills/defaults/devops/changelog-automation.md +580 -0
- package/dist/skills/defaults/devops/cicd.md +314 -0
- package/dist/skills/defaults/devops/cloud.md +263 -0
- package/dist/skills/defaults/devops/code-review-excellence.md +299 -0
- package/dist/skills/defaults/devops/cost-optimization.md +295 -0
- package/dist/skills/defaults/devops/deployment-pipeline-design.md +356 -0
- package/dist/skills/defaults/devops/docker.md +281 -0
- package/dist/skills/defaults/devops/git-workflows.md +205 -0
- package/dist/skills/defaults/devops/github-actions.md +311 -0
- package/dist/skills/defaults/devops/gitlab-ci-patterns.md +266 -0
- package/dist/skills/defaults/devops/hybrid-cloud-networking.md +241 -0
- package/dist/skills/defaults/devops/istio-traffic-management.md +327 -0
- package/dist/skills/defaults/devops/kubernetes.md +339 -0
- package/dist/skills/defaults/devops/linkerd-patterns.md +311 -0
- package/dist/skills/defaults/devops/multi-cloud-architecture.md +181 -0
- package/dist/skills/defaults/devops/observability.md +243 -0
- package/dist/skills/defaults/devops/openapi-spec-generation.md +1024 -0
- package/dist/skills/defaults/devops/postmortem-writing.md +396 -0
- package/dist/skills/defaults/devops/prometheus-configuration.md +265 -0
- package/dist/skills/defaults/devops/secrets-management.md +341 -0
- package/dist/skills/defaults/devops/service-mesh-observability.md +385 -0
- package/dist/skills/defaults/devops/terraform-module-library.md +244 -0
- package/dist/skills/defaults/finance/backtesting-frameworks/SKILL.md +663 -0
- package/dist/skills/defaults/finance/risk-metrics-calculation/SKILL.md +557 -0
- package/dist/skills/defaults/frontend/accessibility-compliance.md +420 -0
- package/dist/skills/defaults/frontend/design-system-patterns.md +337 -0
- package/dist/skills/defaults/frontend/interaction-design.md +327 -0
- package/dist/skills/defaults/frontend/javascript.md +311 -0
- package/dist/skills/defaults/frontend/modern-javascript-patterns.md +927 -0
- package/dist/skills/defaults/frontend/react-native-design.md +440 -0
- package/dist/skills/defaults/frontend/react.md +345 -0
- package/dist/skills/defaults/frontend/responsive-design.md +472 -0
- package/dist/skills/defaults/frontend/tailwind-design-system.md +337 -0
- package/dist/skills/defaults/frontend/typescript-advanced-types.md +724 -0
- package/dist/skills/defaults/frontend/typescript.md +334 -0
- package/dist/skills/defaults/frontend/visual-design-foundations.md +326 -0
- package/dist/skills/defaults/frontend/web-component-design.md +279 -0
- package/dist/skills/defaults/game-development/godot-gdscript-patterns.md +188 -0
- package/dist/skills/defaults/game-development/unity-ecs-patterns.md +594 -0
- package/dist/skills/defaults/kubernetes/gitops-workflow.md +285 -0
- package/dist/skills/defaults/kubernetes/gitops.md +280 -0
- package/dist/skills/defaults/kubernetes/helm-chart-scaffolding.md +553 -0
- package/dist/skills/defaults/kubernetes/helm.md +343 -0
- package/dist/skills/defaults/kubernetes/k8s-manifest-generator.md +501 -0
- package/dist/skills/defaults/kubernetes/k8s-security-policies.md +342 -0
- package/dist/skills/defaults/kubernetes/manifests.md +330 -0
- package/dist/skills/defaults/kubernetes/security.md +337 -0
- package/dist/skills/defaults/llm-application/embedding-strategies.md +608 -0
- package/dist/skills/defaults/llm-application/hybrid-search-implementation.md +570 -0
- package/dist/skills/defaults/llm-application/hybrid-search.md +570 -0
- package/dist/skills/defaults/llm-application/langchain-architecture.md +666 -0
- package/dist/skills/defaults/llm-application/langchain.md +259 -0
- package/dist/skills/defaults/llm-application/llm-evaluation.md +695 -0
- package/dist/skills/defaults/llm-application/prompt-engineering-patterns.md +449 -0
- package/dist/skills/defaults/llm-application/prompt-engineering.md +219 -0
- package/dist/skills/defaults/llm-application/rag-implementation.md +434 -0
- package/dist/skills/defaults/llm-application/similarity-search-patterns.md +560 -0
- package/dist/skills/defaults/llm-application/similarity-search.md +560 -0
- package/dist/skills/defaults/llm-application/vector-index-tuning.md +523 -0
- package/dist/skills/defaults/mobile/mobile-android-design.md +440 -0
- package/dist/skills/defaults/mobile/mobile-ios-design.md +266 -0
- package/dist/skills/defaults/monitoring/distributed-tracing.md +436 -0
- package/dist/skills/defaults/monitoring/grafana-dashboards.md +370 -0
- package/dist/skills/defaults/monitoring/prometheus-configuration.md +379 -0
- package/dist/skills/defaults/monitoring/slo-implementation.md +323 -0
- package/dist/skills/defaults/refactoring/code-refactoring.md +349 -0
- package/dist/skills/defaults/security/anti-reversing-techniques/SKILL.md +559 -0
- package/dist/skills/defaults/security/auditor.md +168 -0
- package/dist/skills/defaults/security/binary-analysis-patterns/SKILL.md +438 -0
- package/dist/skills/defaults/security/memory-forensics/SKILL.md +483 -0
- package/dist/skills/defaults/security/mtls-configuration.md +349 -0
- package/dist/skills/defaults/security/protocol-reverse-engineering/SKILL.md +520 -0
- package/dist/skills/defaults/security/sast-configuration.md +182 -0
- package/dist/skills/defaults/security/security.md +313 -0
- package/dist/skills/defaults/security/stride-analysis.md +273 -0
- package/dist/skills/defaults/security/threat-mitigation-mapping.md +290 -0
- package/dist/skills/defaults/systems/bash-defensive-patterns/SKILL.md +539 -0
- package/dist/skills/defaults/systems/bats-testing-patterns/SKILL.md +631 -0
- package/dist/skills/defaults/systems/go-concurrency-patterns.md +657 -0
- package/dist/skills/defaults/systems/memory-safety-patterns.md +605 -0
- package/dist/skills/defaults/systems/rust-async-patterns.md +519 -0
- package/dist/skills/defaults/systems/shellcheck-configuration/SKILL.md +456 -0
- package/dist/skills/defaults/team-collaboration/multi-reviewer-patterns.md +126 -0
- package/dist/skills/defaults/team-collaboration/parallel-feature-development.md +151 -0
- package/dist/skills/defaults/testing/javascript-testing-patterns.md +1021 -0
- package/dist/skills/defaults/testing/python-testing-patterns.md +351 -0
- package/dist/skills/defaults/testing/testing.md +332 -0
- package/dist/skills/defaults/workflows/context-driven-development.md +384 -0
- package/dist/skills/defaults/workflows/track-management.md +592 -0
- package/dist/skills/defaults/workflows/workflow-patterns.md +622 -0
- package/dist/skills/index.d.ts +11 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +129 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/utils/character.js +4 -4
- package/dist/utils/character.js.map +1 -1
- package/dist/utils/inputbar.d.ts.map +1 -1
- package/dist/utils/inputbar.js +7 -0
- package/dist/utils/inputbar.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: python-testing-patterns
|
|
3
|
+
description: Implement comprehensive testing strategies with pytest, fixtures, mocking, and test-driven development. Use when writing Python tests, setting up test suites, or implementing testing best practices.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Python Testing Patterns
|
|
7
|
+
|
|
8
|
+
Comprehensive guide to implementing robust testing strategies in Python using pytest, fixtures, mocking, parameterization, and test-driven development practices.
|
|
9
|
+
|
|
10
|
+
## When to Use This Skill
|
|
11
|
+
|
|
12
|
+
- Writing unit tests for Python code
|
|
13
|
+
- Setting up test suites and test infrastructure
|
|
14
|
+
- Implementing test-driven development (TDD)
|
|
15
|
+
- Creating integration tests for APIs and services
|
|
16
|
+
- Mocking external dependencies and services
|
|
17
|
+
- Testing async code and concurrent operations
|
|
18
|
+
- Setting up continuous testing in CI/CD
|
|
19
|
+
- Implementing property-based testing
|
|
20
|
+
- Testing database operations
|
|
21
|
+
- Debugging failing tests
|
|
22
|
+
|
|
23
|
+
## Core Concepts
|
|
24
|
+
|
|
25
|
+
### 1. Test Types
|
|
26
|
+
|
|
27
|
+
- **Unit Tests**: Test individual functions/classes in isolation
|
|
28
|
+
- **Integration Tests**: Test interaction between components
|
|
29
|
+
- **Functional Tests**: Test complete features end-to-end
|
|
30
|
+
- **Performance Tests**: Measure speed and resource usage
|
|
31
|
+
|
|
32
|
+
### 2. Test Structure (AAA Pattern)
|
|
33
|
+
|
|
34
|
+
- **Arrange**: Set up test data and preconditions
|
|
35
|
+
- **Act**: Execute the code under test
|
|
36
|
+
- **Assert**: Verify the results
|
|
37
|
+
|
|
38
|
+
### 3. Test Coverage
|
|
39
|
+
|
|
40
|
+
- Measure what code is exercised by tests
|
|
41
|
+
- Identify untested code paths
|
|
42
|
+
- Aim for meaningful coverage, not just high percentages
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
def add(a, b):
|
|
48
|
+
return a + b
|
|
49
|
+
|
|
50
|
+
def test_add():
|
|
51
|
+
result = add(2, 3)
|
|
52
|
+
assert result == 5
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Fundamental Patterns
|
|
56
|
+
|
|
57
|
+
### Pattern 1: Basic pytest Tests
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import pytest
|
|
61
|
+
|
|
62
|
+
class Calculator:
|
|
63
|
+
def add(self, a: float, b: float) -> float:
|
|
64
|
+
return a + b
|
|
65
|
+
|
|
66
|
+
def divide(self, a: float, b: float) -> float:
|
|
67
|
+
if b == 0:
|
|
68
|
+
raise ValueError("Cannot divide by zero")
|
|
69
|
+
return a / b
|
|
70
|
+
|
|
71
|
+
def test_addition():
|
|
72
|
+
calc = Calculator()
|
|
73
|
+
assert calc.add(2, 3) == 5
|
|
74
|
+
|
|
75
|
+
def test_division_by_zero():
|
|
76
|
+
calc = Calculator()
|
|
77
|
+
with pytest.raises(ValueError, match="Cannot divide by zero"):
|
|
78
|
+
calc.divide(5, 0)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Pattern 2: Fixtures for Setup and Teardown
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import pytest
|
|
85
|
+
from typing import Generator
|
|
86
|
+
|
|
87
|
+
class Database:
|
|
88
|
+
def __init__(self, connection_string: str):
|
|
89
|
+
self.connection_string = connection_string
|
|
90
|
+
self.connected = False
|
|
91
|
+
|
|
92
|
+
def connect(self):
|
|
93
|
+
self.connected = True
|
|
94
|
+
|
|
95
|
+
def disconnect(self):
|
|
96
|
+
self.connected = False
|
|
97
|
+
|
|
98
|
+
@pytest.fixture
|
|
99
|
+
def db() -> Generator[Database, None, None]:
|
|
100
|
+
database = Database("sqlite:///:memory:")
|
|
101
|
+
database.connect()
|
|
102
|
+
yield database
|
|
103
|
+
database.disconnect()
|
|
104
|
+
|
|
105
|
+
def test_database_query(db):
|
|
106
|
+
assert db.connected is True
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Pattern 3: Parameterized Tests
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
import pytest
|
|
113
|
+
|
|
114
|
+
def is_valid_email(email: str) -> bool:
|
|
115
|
+
return "@" in email and "." in email.split("@")[1]
|
|
116
|
+
|
|
117
|
+
@pytest.mark.parametrize("email,expected", [
|
|
118
|
+
("user@example.com", True),
|
|
119
|
+
("invalid.email", False),
|
|
120
|
+
("@example.com", False),
|
|
121
|
+
])
|
|
122
|
+
def test_email_validation(email, expected):
|
|
123
|
+
assert is_valid_email(email) == expected
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Pattern 4: Mocking with unittest.mock
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import pytest
|
|
130
|
+
from unittest.mock import Mock, patch
|
|
131
|
+
|
|
132
|
+
class APIClient:
|
|
133
|
+
def __init__(self, base_url: str):
|
|
134
|
+
self.base_url = base_url
|
|
135
|
+
|
|
136
|
+
def get_user(self, user_id: int) -> dict:
|
|
137
|
+
import requests
|
|
138
|
+
response = requests.get(f"{self.base_url}/users/{user_id}")
|
|
139
|
+
response.raise_for_status()
|
|
140
|
+
return response.json()
|
|
141
|
+
|
|
142
|
+
def test_get_user_success():
|
|
143
|
+
client = APIClient("https://api.example.com")
|
|
144
|
+
|
|
145
|
+
mock_response = Mock()
|
|
146
|
+
mock_response.json.return_value = {"id": 1, "name": "John Doe"}
|
|
147
|
+
mock_response.raise_for_status.return_value = None
|
|
148
|
+
|
|
149
|
+
with patch("requests.get", return_value=mock_response) as mock_get:
|
|
150
|
+
user = client.get_user(1)
|
|
151
|
+
assert user["id"] == 1
|
|
152
|
+
mock_get.assert_called_once_with("https://api.example.com/users/1")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Pattern 5: Testing Exceptions
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
import pytest
|
|
159
|
+
|
|
160
|
+
def divide(a: float, b: float) -> float:
|
|
161
|
+
if b == 0:
|
|
162
|
+
raise ZeroDivisionError("Division by zero")
|
|
163
|
+
return a / b
|
|
164
|
+
|
|
165
|
+
def test_zero_division():
|
|
166
|
+
with pytest.raises(ZeroDivisionError):
|
|
167
|
+
divide(10, 0)
|
|
168
|
+
|
|
169
|
+
def test_exception_message():
|
|
170
|
+
with pytest.raises(ZeroDivisionError, match="Division by zero"):
|
|
171
|
+
divide(5, 0)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Advanced Patterns
|
|
175
|
+
|
|
176
|
+
### Pattern 6: Testing Async Code
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
import pytest
|
|
180
|
+
import asyncio
|
|
181
|
+
|
|
182
|
+
async def fetch_data(url: str) -> dict:
|
|
183
|
+
await asyncio.sleep(0.1)
|
|
184
|
+
return {"url": url, "data": "result"}
|
|
185
|
+
|
|
186
|
+
@pytest.mark.asyncio
|
|
187
|
+
async def test_fetch_data():
|
|
188
|
+
result = await fetch_data("https://api.example.com")
|
|
189
|
+
assert result["url"] == "https://api.example.com"
|
|
190
|
+
|
|
191
|
+
@pytest.mark.asyncio
|
|
192
|
+
async def test_concurrent_fetches():
|
|
193
|
+
urls = ["url1", "url2", "url3"]
|
|
194
|
+
tasks = [fetch_data(url) for url in urls]
|
|
195
|
+
results = await asyncio.gather(*tasks)
|
|
196
|
+
assert len(results) == 3
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Pattern 7: Monkeypatch for Testing
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
import os
|
|
203
|
+
import pytest
|
|
204
|
+
|
|
205
|
+
def get_database_url() -> str:
|
|
206
|
+
return os.environ.get("DATABASE_URL", "sqlite:///:memory:")
|
|
207
|
+
|
|
208
|
+
def test_database_url_custom(monkeypatch):
|
|
209
|
+
monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/test")
|
|
210
|
+
assert get_database_url() == "postgresql://localhost/test"
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Pattern 8: Temporary Files and Directories
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
import pytest
|
|
217
|
+
from pathlib import Path
|
|
218
|
+
|
|
219
|
+
def save_data(filepath: Path, data: str):
|
|
220
|
+
filepath.write_text(data)
|
|
221
|
+
|
|
222
|
+
def test_file_operations(tmp_path):
|
|
223
|
+
test_file = tmp_path / "test_data.txt"
|
|
224
|
+
save_data(test_file, "Hello, World!")
|
|
225
|
+
assert test_file.exists()
|
|
226
|
+
assert test_file.read_text() == "Hello, World!"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Pattern 9: Property-Based Testing
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from hypothesis import given, strategies as st
|
|
233
|
+
|
|
234
|
+
def reverse_string(s: str) -> str:
|
|
235
|
+
return s[::-1]
|
|
236
|
+
|
|
237
|
+
@given(st.text())
|
|
238
|
+
def test_reverse_twice_is_original(s):
|
|
239
|
+
assert reverse_string(reverse_string(s)) == s
|
|
240
|
+
|
|
241
|
+
@given(st.integers(), st.integers())
|
|
242
|
+
def test_addition_commutative(a, b):
|
|
243
|
+
assert a + b == b + a
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Pattern 10: Testing Retry Behavior
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from unittest.mock import Mock
|
|
250
|
+
|
|
251
|
+
def test_retries_on_transient_error():
|
|
252
|
+
client = Mock()
|
|
253
|
+
client.request.side_effect = [
|
|
254
|
+
ConnectionError("Failed"),
|
|
255
|
+
ConnectionError("Failed"),
|
|
256
|
+
{"status": "ok"},
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
service = ServiceWithRetry(client, max_retries=3)
|
|
260
|
+
result = service.fetch()
|
|
261
|
+
|
|
262
|
+
assert result == {"status": "ok"}
|
|
263
|
+
assert client.request.call_count == 3
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Pattern 11: Mocking Time with Freezegun
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
from freezegun import freeze_time
|
|
270
|
+
from datetime import datetime
|
|
271
|
+
|
|
272
|
+
@freeze_time("2026-01-15 10:00:00")
|
|
273
|
+
def test_token_expiry():
|
|
274
|
+
token = create_token(expires_in_seconds=3600)
|
|
275
|
+
assert token.expires_at == datetime(2026, 1, 15, 11, 0, 0)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Test Design Principles
|
|
279
|
+
|
|
280
|
+
### One Behavior Per Test
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
# BAD - testing multiple behaviors
|
|
284
|
+
def test_user_service():
|
|
285
|
+
user = service.create_user(data)
|
|
286
|
+
assert user.id is not None
|
|
287
|
+
assert user.email == data["email"]
|
|
288
|
+
|
|
289
|
+
# GOOD - focused tests
|
|
290
|
+
def test_create_user_assigns_id():
|
|
291
|
+
user = service.create_user(data)
|
|
292
|
+
assert user.id is not None
|
|
293
|
+
|
|
294
|
+
def test_create_user_stores_email():
|
|
295
|
+
user = service.create_user(data)
|
|
296
|
+
assert user.email == data["email"]
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Testing Best Practices
|
|
300
|
+
|
|
301
|
+
1. **Write tests first** (TDD) or alongside code
|
|
302
|
+
2. **One assertion per test** when possible
|
|
303
|
+
3. **Use descriptive test names** that explain behavior
|
|
304
|
+
4. **Keep tests independent** and isolated
|
|
305
|
+
5. **Use fixtures** for setup and teardown
|
|
306
|
+
6. **Mock external dependencies** appropriately
|
|
307
|
+
7. **Parametrize tests** to reduce duplication
|
|
308
|
+
8. **Test edge cases** and error conditions
|
|
309
|
+
9. **Measure coverage** but focus on quality
|
|
310
|
+
10. **Run tests in CI/CD** on every commit
|
|
311
|
+
|
|
312
|
+
## Test Organization
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
tests/
|
|
316
|
+
conftest.py # Shared fixtures
|
|
317
|
+
test_unit/ # Unit tests
|
|
318
|
+
test_integration/ # Integration tests
|
|
319
|
+
test_e2e/ # End-to-end tests
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## CI/CD Integration
|
|
323
|
+
|
|
324
|
+
```yaml
|
|
325
|
+
name: Tests
|
|
326
|
+
|
|
327
|
+
on: [push, pull_request]
|
|
328
|
+
|
|
329
|
+
jobs:
|
|
330
|
+
test:
|
|
331
|
+
runs-on: ubuntu-latest
|
|
332
|
+
steps:
|
|
333
|
+
- uses: actions/checkout@v3
|
|
334
|
+
- name: Run tests
|
|
335
|
+
run: pytest --cov=myapp --cov-report=xml
|
|
336
|
+
- name: Upload coverage
|
|
337
|
+
uses: codecov/codecov-action@v3
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Configuration
|
|
341
|
+
|
|
342
|
+
```toml
|
|
343
|
+
# pyproject.toml
|
|
344
|
+
[tool.pytest.ini_options]
|
|
345
|
+
testpaths = ["tests"]
|
|
346
|
+
addopts = ["-v", "--cov=myapp", "--cov-report=term-missing"]
|
|
347
|
+
|
|
348
|
+
[tool.coverage.run]
|
|
349
|
+
source = ["myapp"]
|
|
350
|
+
omit = ["*/tests/*", "*/migrations/*"]
|
|
351
|
+
```
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing
|
|
3
|
+
description: Write effective unit, integration, and E2E tests with modern testing patterns and best practices.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Testing Best Practices — CoWorker Edition
|
|
7
|
+
|
|
8
|
+
Write tests that give you confidence.
|
|
9
|
+
|
|
10
|
+
## When to Use This Skill
|
|
11
|
+
|
|
12
|
+
- Writing new tests
|
|
13
|
+
- Improving test coverage
|
|
14
|
+
- Setting up test infrastructure
|
|
15
|
+
- Debugging test failures
|
|
16
|
+
|
|
17
|
+
## Core Concepts
|
|
18
|
+
|
|
19
|
+
### 1. Unit Tests
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
23
|
+
import { UserService } from './UserService';
|
|
24
|
+
import { UserRepository } from './UserRepository';
|
|
25
|
+
|
|
26
|
+
describe('UserService', () => {
|
|
27
|
+
let userService: UserService;
|
|
28
|
+
let mockRepository: UserRepository;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
// Create mock
|
|
32
|
+
mockRepository = {
|
|
33
|
+
findById: vi.fn(),
|
|
34
|
+
save: vi.fn(),
|
|
35
|
+
delete: vi.fn()
|
|
36
|
+
} as UserRepository;
|
|
37
|
+
|
|
38
|
+
userService = new UserService(mockRepository);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('getUser', () => {
|
|
42
|
+
it('should return user when found', async () => {
|
|
43
|
+
const mockUser = { id: '1', name: 'John' };
|
|
44
|
+
mockRepository.findById.mockResolvedValue(mockUser);
|
|
45
|
+
|
|
46
|
+
const result = await userService.getUser('1');
|
|
47
|
+
|
|
48
|
+
expect(result).toEqual(mockUser);
|
|
49
|
+
expect(mockRepository.findById).toHaveBeenCalledWith('1');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should throw when user not found', async () => {
|
|
53
|
+
mockRepository.findById.mockResolvedValue(null);
|
|
54
|
+
|
|
55
|
+
await expect(userService.getUser('999')).rejects.toThrow('User not found');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('createUser', () => {
|
|
60
|
+
it('should create user with hashed password', async () => {
|
|
61
|
+
mockRepository.save.mockResolvedValue({ id: '1', email: 'test@test.com' });
|
|
62
|
+
|
|
63
|
+
const result = await userService.createUser({
|
|
64
|
+
email: 'test@test.com',
|
|
65
|
+
password: 'plaintext123'
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(mockRepository.save).toHaveBeenCalled();
|
|
69
|
+
const savedUser = mockRepository.save.mock.calls[0][0];
|
|
70
|
+
expect(savedUser.passwordHash).toBeDefined();
|
|
71
|
+
expect(savedUser.passwordHash).not.toBe('plaintext123');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 2. Integration Tests
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
81
|
+
import { setupTestDatabase, teardownTestDatabase } from './testUtils';
|
|
82
|
+
import { app } from '../src/app';
|
|
83
|
+
import request from 'supertest';
|
|
84
|
+
|
|
85
|
+
describe('API Integration Tests', () => {
|
|
86
|
+
beforeAll(async () => {
|
|
87
|
+
await setupTestDatabase();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterAll(async () => {
|
|
91
|
+
await teardownTestDatabase();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
beforeEach(async () => {
|
|
95
|
+
await clearTestData();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('POST /api/users', () => {
|
|
99
|
+
it('should create a new user', async () => {
|
|
100
|
+
const response = await request(app)
|
|
101
|
+
.post('/api/users')
|
|
102
|
+
.send({
|
|
103
|
+
email: 'test@example.com',
|
|
104
|
+
name: 'Test User'
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(response.status).toBe(201);
|
|
108
|
+
expect(response.body).toMatchObject({
|
|
109
|
+
email: 'test@example.com',
|
|
110
|
+
name: 'Test User'
|
|
111
|
+
});
|
|
112
|
+
expect(response.body.id).toBeDefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should return 400 for invalid email', async () => {
|
|
116
|
+
const response = await request(app)
|
|
117
|
+
.post('/api/users')
|
|
118
|
+
.send({
|
|
119
|
+
email: 'invalid-email',
|
|
120
|
+
name: 'Test'
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(response.status).toBe(400);
|
|
124
|
+
expect(response.body.error).toContain('email');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('GET /api/users/:id', () => {
|
|
129
|
+
it('should return user by id', async () => {
|
|
130
|
+
// First create a user
|
|
131
|
+
const created = await request(app)
|
|
132
|
+
.post('/api/users')
|
|
133
|
+
.send({ email: 'test@test.com', name: 'Test' });
|
|
134
|
+
|
|
135
|
+
const response = await request(app)
|
|
136
|
+
.get(`/api/users/${created.body.id}`);
|
|
137
|
+
|
|
138
|
+
expect(response.status).toBe(200);
|
|
139
|
+
expect(response.body.email).toBe('test@test.com');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should return 404 for non-existent user', async () => {
|
|
143
|
+
const response = await request(app)
|
|
144
|
+
.get('/api/users/999999');
|
|
145
|
+
|
|
146
|
+
expect(response.status).toBe(404);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 3. E2E Tests with Playwright
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { test, expect } from '@playwright/test';
|
|
156
|
+
|
|
157
|
+
test.describe('User Registration Flow', () => {
|
|
158
|
+
test('should register a new user', async ({ page }) => {
|
|
159
|
+
await page.goto('/register');
|
|
160
|
+
|
|
161
|
+
await page.fill('[data-testid="email"]', 'newuser@example.com');
|
|
162
|
+
await page.fill('[data-testid="password"]', 'password123');
|
|
163
|
+
await page.fill('[data-testid="confirm-password"]', 'password123');
|
|
164
|
+
await page.click('[data-testid="register-button"]');
|
|
165
|
+
|
|
166
|
+
// Should redirect to dashboard
|
|
167
|
+
await expect(page).toHaveURL('/dashboard');
|
|
168
|
+
|
|
169
|
+
// Should show success message
|
|
170
|
+
await expect(page.locator('.toast')).toContainText('Welcome');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('should show validation errors', async ({ page }) => {
|
|
174
|
+
await page.goto('/register');
|
|
175
|
+
|
|
176
|
+
await page.fill('[data-testid="email"]', 'invalid');
|
|
177
|
+
await page.click('[data-testid="register-button"]');
|
|
178
|
+
|
|
179
|
+
await expect(page.locator('[data-testid="email-error"]'))
|
|
180
|
+
.toContainText('Valid email required');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test.describe('Authentication', () => {
|
|
185
|
+
test.beforeEach(async ({ page }) => {
|
|
186
|
+
// Login before each test
|
|
187
|
+
await page.goto('/login');
|
|
188
|
+
await page.fill('[data-testid="email"]', 'test@example.com');
|
|
189
|
+
await page.fill('[data-testid="password"]', 'password123');
|
|
190
|
+
await page.click('[data-testid="login-button"]');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('should protect dashboard', async ({ page }) => {
|
|
194
|
+
await page.goto('/dashboard');
|
|
195
|
+
await expect(page).toHaveURL('/dashboard');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### 4. Test Fixtures
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { test as base, fixtures } from '@playwright/test';
|
|
204
|
+
|
|
205
|
+
interface UserFixtures {
|
|
206
|
+
testUser: { email: string; password: string };
|
|
207
|
+
adminUser: { email: string; role: string };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const myFixtures = fixtures({
|
|
211
|
+
testUser: async ({}, use) => {
|
|
212
|
+
const user = {
|
|
213
|
+
email: 'test@example.com',
|
|
214
|
+
password: 'testpass123'
|
|
215
|
+
};
|
|
216
|
+
await createTestUser(user);
|
|
217
|
+
await use(user);
|
|
218
|
+
await deleteTestUser(user.email);
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
adminUser: async ({}, use) => {
|
|
222
|
+
const user = {
|
|
223
|
+
email: 'admin@example.com',
|
|
224
|
+
role: 'admin'
|
|
225
|
+
};
|
|
226
|
+
await createAdminUser(user);
|
|
227
|
+
await use(user);
|
|
228
|
+
await deleteTestUser(user.email);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
export const test = base.extend<UserFixtures>(myFixtures);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### 5. Mocking External Services
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { vi } from 'vitest';
|
|
239
|
+
import nock from 'nock';
|
|
240
|
+
|
|
241
|
+
describe('PaymentService', () => {
|
|
242
|
+
afterEach(() => {
|
|
243
|
+
nock.cleanAll();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should process payment via Stripe', async () => {
|
|
247
|
+
// Mock Stripe API
|
|
248
|
+
nock('https://api.stripe.com')
|
|
249
|
+
.post('/v1/charges')
|
|
250
|
+
.reply(200, {
|
|
251
|
+
id: 'ch_123',
|
|
252
|
+
status: 'succeeded',
|
|
253
|
+
amount: 1000
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const service = new PaymentService();
|
|
257
|
+
const result = await service.processPayment({
|
|
258
|
+
amount: 1000,
|
|
259
|
+
currency: 'usd',
|
|
260
|
+
token: 'tok_visa'
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
expect(result.status).toBe('succeeded');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should handle payment failures', async () => {
|
|
267
|
+
nock('https://api.stripe.com')
|
|
268
|
+
.post('/v1/charges')
|
|
269
|
+
.reply(402, {
|
|
270
|
+
error: {
|
|
271
|
+
code: 'card_declined',
|
|
272
|
+
message: 'Card was declined'
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const service = new PaymentService();
|
|
277
|
+
|
|
278
|
+
await expect(service.processPayment({
|
|
279
|
+
amount: 1000,
|
|
280
|
+
currency: 'usd',
|
|
281
|
+
token: 'tok_chargeDeclined'
|
|
282
|
+
})).rejects.toThrow('Payment failed');
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### 6. Test Coverage
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// vitest.config.ts
|
|
291
|
+
import { defineConfig } from 'vitest/config';
|
|
292
|
+
|
|
293
|
+
export default defineConfig({
|
|
294
|
+
test: {
|
|
295
|
+
coverage: {
|
|
296
|
+
provider: 'v8',
|
|
297
|
+
reporter: ['text', 'json', 'html'],
|
|
298
|
+
include: [
|
|
299
|
+
'src/**/*.ts',
|
|
300
|
+
'!src/**/*.d.ts'
|
|
301
|
+
],
|
|
302
|
+
exclude: [
|
|
303
|
+
'src/**/*.test.ts',
|
|
304
|
+
'src/**/*.spec.ts'
|
|
305
|
+
]
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Run with coverage
|
|
311
|
+
// npm run test:coverage
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Best Practices
|
|
315
|
+
|
|
316
|
+
1. **AAA Pattern** - Arrange, Act, Assert
|
|
317
|
+
2. **One assertion per test** - Better failure messages
|
|
318
|
+
3. **Test behavior, not implementation** - Refactor safe
|
|
319
|
+
4. **Descriptive names** - Document what's tested
|
|
320
|
+
5. **Fast tests** - No network calls in unit tests
|
|
321
|
+
6. **Isolation** - Tests don't depend on each other
|
|
322
|
+
7. **Mocks external services** - Speed and reliability
|
|
323
|
+
|
|
324
|
+
## Common Mistakes
|
|
325
|
+
|
|
326
|
+
- Testing implementation details
|
|
327
|
+
- Not cleaning up test data
|
|
328
|
+
- Over-mocking
|
|
329
|
+
- Slow test suites
|
|
330
|
+
- Brittle selectors
|
|
331
|
+
- Not testing edge cases
|
|
332
|
+
- Skipping E2E tests
|