opencode-metis 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/README.md +140 -0
- package/dist/cli.cjs +63 -0
- package/dist/mcp-server.cjs +51 -0
- package/dist/plugin.cjs +4 -0
- package/dist/worker.cjs +224 -0
- package/opencode/agent/the-analyst/feature-prioritization.md +66 -0
- package/opencode/agent/the-analyst/market-research.md +77 -0
- package/opencode/agent/the-analyst/project-coordination.md +81 -0
- package/opencode/agent/the-analyst/requirements-analysis.md +77 -0
- package/opencode/agent/the-architect/compatibility-review.md +138 -0
- package/opencode/agent/the-architect/complexity-review.md +137 -0
- package/opencode/agent/the-architect/quality-review.md +67 -0
- package/opencode/agent/the-architect/security-review.md +127 -0
- package/opencode/agent/the-architect/system-architecture.md +119 -0
- package/opencode/agent/the-architect/system-documentation.md +83 -0
- package/opencode/agent/the-architect/technology-research.md +85 -0
- package/opencode/agent/the-chief.md +79 -0
- package/opencode/agent/the-designer/accessibility-implementation.md +101 -0
- package/opencode/agent/the-designer/design-foundation.md +74 -0
- package/opencode/agent/the-designer/interaction-architecture.md +75 -0
- package/opencode/agent/the-designer/user-research.md +70 -0
- package/opencode/agent/the-meta-agent.md +155 -0
- package/opencode/agent/the-platform-engineer/ci-cd-pipelines.md +109 -0
- package/opencode/agent/the-platform-engineer/containerization.md +106 -0
- package/opencode/agent/the-platform-engineer/data-architecture.md +81 -0
- package/opencode/agent/the-platform-engineer/dependency-review.md +144 -0
- package/opencode/agent/the-platform-engineer/deployment-automation.md +81 -0
- package/opencode/agent/the-platform-engineer/infrastructure-as-code.md +107 -0
- package/opencode/agent/the-platform-engineer/performance-tuning.md +82 -0
- package/opencode/agent/the-platform-engineer/pipeline-engineering.md +81 -0
- package/opencode/agent/the-platform-engineer/production-monitoring.md +105 -0
- package/opencode/agent/the-qa-engineer/exploratory-testing.md +66 -0
- package/opencode/agent/the-qa-engineer/performance-testing.md +81 -0
- package/opencode/agent/the-qa-engineer/quality-assurance.md +77 -0
- package/opencode/agent/the-qa-engineer/test-execution.md +66 -0
- package/opencode/agent/the-software-engineer/api-development.md +78 -0
- package/opencode/agent/the-software-engineer/component-development.md +79 -0
- package/opencode/agent/the-software-engineer/concurrency-review.md +141 -0
- package/opencode/agent/the-software-engineer/domain-modeling.md +66 -0
- package/opencode/agent/the-software-engineer/performance-optimization.md +113 -0
- package/opencode/command/analyze.md +149 -0
- package/opencode/command/constitution.md +178 -0
- package/opencode/command/debug.md +194 -0
- package/opencode/command/document.md +178 -0
- package/opencode/command/implement.md +225 -0
- package/opencode/command/refactor.md +207 -0
- package/opencode/command/review.md +229 -0
- package/opencode/command/simplify.md +267 -0
- package/opencode/command/specify.md +191 -0
- package/opencode/command/validate.md +224 -0
- package/opencode/skill/accessibility-design/SKILL.md +566 -0
- package/opencode/skill/accessibility-design/checklists/wcag-checklist.md +435 -0
- package/opencode/skill/agent-coordination/SKILL.md +224 -0
- package/opencode/skill/api-contract-design/SKILL.md +550 -0
- package/opencode/skill/api-contract-design/templates/graphql-schema-template.md +818 -0
- package/opencode/skill/api-contract-design/templates/rest-api-template.md +417 -0
- package/opencode/skill/architecture-design/SKILL.md +160 -0
- package/opencode/skill/architecture-design/examples/architecture-examples.md +170 -0
- package/opencode/skill/architecture-design/template.md +749 -0
- package/opencode/skill/architecture-design/validation.md +99 -0
- package/opencode/skill/architecture-selection/SKILL.md +522 -0
- package/opencode/skill/architecture-selection/examples/adrs/001-example-adr.md +71 -0
- package/opencode/skill/architecture-selection/examples/architecture-patterns.md +239 -0
- package/opencode/skill/bug-diagnosis/SKILL.md +235 -0
- package/opencode/skill/code-quality-review/SKILL.md +337 -0
- package/opencode/skill/code-quality-review/examples/anti-patterns.md +629 -0
- package/opencode/skill/code-quality-review/reference.md +322 -0
- package/opencode/skill/code-review/SKILL.md +363 -0
- package/opencode/skill/code-review/reference.md +450 -0
- package/opencode/skill/codebase-analysis/SKILL.md +139 -0
- package/opencode/skill/codebase-navigation/SKILL.md +227 -0
- package/opencode/skill/codebase-navigation/examples/exploration-patterns.md +263 -0
- package/opencode/skill/coding-conventions/SKILL.md +178 -0
- package/opencode/skill/coding-conventions/checklists/accessibility-checklist.md +176 -0
- package/opencode/skill/coding-conventions/checklists/performance-checklist.md +154 -0
- package/opencode/skill/coding-conventions/checklists/security-checklist.md +127 -0
- package/opencode/skill/constitution-validation/SKILL.md +315 -0
- package/opencode/skill/constitution-validation/examples/CONSTITUTION.md +202 -0
- package/opencode/skill/constitution-validation/reference/rule-patterns.md +328 -0
- package/opencode/skill/constitution-validation/template.md +115 -0
- package/opencode/skill/context-preservation/SKILL.md +445 -0
- package/opencode/skill/data-modeling/SKILL.md +385 -0
- package/opencode/skill/data-modeling/templates/schema-design-template.md +268 -0
- package/opencode/skill/deployment-pipeline-design/SKILL.md +579 -0
- package/opencode/skill/deployment-pipeline-design/templates/pipeline-template.md +633 -0
- package/opencode/skill/documentation-extraction/SKILL.md +259 -0
- package/opencode/skill/documentation-sync/SKILL.md +431 -0
- package/opencode/skill/domain-driven-design/SKILL.md +509 -0
- package/opencode/skill/domain-driven-design/examples/ddd-patterns.md +688 -0
- package/opencode/skill/domain-driven-design/reference.md +465 -0
- package/opencode/skill/drift-detection/SKILL.md +383 -0
- package/opencode/skill/drift-detection/reference.md +340 -0
- package/opencode/skill/error-recovery/SKILL.md +162 -0
- package/opencode/skill/error-recovery/examples/error-patterns.md +484 -0
- package/opencode/skill/feature-prioritization/SKILL.md +419 -0
- package/opencode/skill/feature-prioritization/examples/rice-template.md +139 -0
- package/opencode/skill/feature-prioritization/reference.md +256 -0
- package/opencode/skill/git-workflow/SKILL.md +453 -0
- package/opencode/skill/implementation-planning/SKILL.md +215 -0
- package/opencode/skill/implementation-planning/examples/phase-examples.md +217 -0
- package/opencode/skill/implementation-planning/template.md +220 -0
- package/opencode/skill/implementation-planning/validation.md +88 -0
- package/opencode/skill/implementation-verification/SKILL.md +272 -0
- package/opencode/skill/knowledge-capture/SKILL.md +265 -0
- package/opencode/skill/knowledge-capture/reference/knowledge-capture.md +402 -0
- package/opencode/skill/knowledge-capture/reference.md +444 -0
- package/opencode/skill/knowledge-capture/templates/domain-template.md +325 -0
- package/opencode/skill/knowledge-capture/templates/interface-template.md +255 -0
- package/opencode/skill/knowledge-capture/templates/pattern-template.md +144 -0
- package/opencode/skill/observability-design/SKILL.md +291 -0
- package/opencode/skill/observability-design/references/monitoring-patterns.md +461 -0
- package/opencode/skill/pattern-detection/SKILL.md +171 -0
- package/opencode/skill/pattern-detection/examples/common-patterns.md +359 -0
- package/opencode/skill/performance-analysis/SKILL.md +266 -0
- package/opencode/skill/performance-analysis/references/profiling-tools.md +499 -0
- package/opencode/skill/requirements-analysis/SKILL.md +139 -0
- package/opencode/skill/requirements-analysis/examples/good-prd.md +66 -0
- package/opencode/skill/requirements-analysis/template.md +177 -0
- package/opencode/skill/requirements-analysis/validation.md +69 -0
- package/opencode/skill/requirements-elicitation/SKILL.md +518 -0
- package/opencode/skill/requirements-elicitation/examples/interview-questions.md +226 -0
- package/opencode/skill/requirements-elicitation/examples/user-stories.md +414 -0
- package/opencode/skill/safe-refactoring/SKILL.md +312 -0
- package/opencode/skill/safe-refactoring/reference/code-smells.md +347 -0
- package/opencode/skill/security-assessment/SKILL.md +421 -0
- package/opencode/skill/security-assessment/checklists/security-review-checklist.md +285 -0
- package/opencode/skill/specification-management/SKILL.md +143 -0
- package/opencode/skill/specification-management/readme-template.md +32 -0
- package/opencode/skill/specification-management/reference.md +115 -0
- package/opencode/skill/specification-management/spec.py +229 -0
- package/opencode/skill/specification-validation/SKILL.md +397 -0
- package/opencode/skill/specification-validation/reference/3cs-framework.md +306 -0
- package/opencode/skill/specification-validation/reference/ambiguity-detection.md +132 -0
- package/opencode/skill/specification-validation/reference/constitution-validation.md +301 -0
- package/opencode/skill/specification-validation/reference/drift-detection.md +383 -0
- package/opencode/skill/task-delegation/SKILL.md +607 -0
- package/opencode/skill/task-delegation/examples/file-coordination.md +495 -0
- package/opencode/skill/task-delegation/examples/parallel-research.md +337 -0
- package/opencode/skill/task-delegation/examples/sequential-build.md +504 -0
- package/opencode/skill/task-delegation/reference.md +825 -0
- package/opencode/skill/tech-stack-detection/SKILL.md +89 -0
- package/opencode/skill/tech-stack-detection/references/framework-signatures.md +598 -0
- package/opencode/skill/technical-writing/SKILL.md +190 -0
- package/opencode/skill/technical-writing/templates/adr-template.md +205 -0
- package/opencode/skill/technical-writing/templates/system-doc-template.md +380 -0
- package/opencode/skill/test-design/SKILL.md +464 -0
- package/opencode/skill/test-design/examples/test-pyramid.md +724 -0
- package/opencode/skill/testing/SKILL.md +213 -0
- package/opencode/skill/testing/examples/test-pyramid.md +724 -0
- package/opencode/skill/user-insight-synthesis/SKILL.md +576 -0
- package/opencode/skill/user-insight-synthesis/templates/research-plan-template.md +217 -0
- package/opencode/skill/user-research/SKILL.md +508 -0
- package/opencode/skill/user-research/examples/interview-questions.md +265 -0
- package/opencode/skill/user-research/examples/personas.md +267 -0
- package/opencode/skill/vibe-security/SKILL.md +654 -0
- package/package.json +45 -0
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
# Test Pyramid Implementation Guide
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
This guide provides practical examples for implementing tests at each level of the test pyramid. Use these patterns when designing test suites to achieve optimal coverage with fast feedback loops.
|
|
6
|
+
|
|
7
|
+
## Test Pyramid Overview
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
/\
|
|
11
|
+
/ \ E2E: 5-10%
|
|
12
|
+
/ \ Validate critical user journeys
|
|
13
|
+
/------\ across the entire system
|
|
14
|
+
/ \
|
|
15
|
+
/ \ Integration: 20-30%
|
|
16
|
+
/ \ Verify component interactions
|
|
17
|
+
/--------------\ and external dependencies
|
|
18
|
+
/ \
|
|
19
|
+
/ Unit: 60-70% \ Test isolated business logic
|
|
20
|
+
/==================\ Fast, deterministic, focused
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Why This Distribution?
|
|
24
|
+
|
|
25
|
+
| Level | Speed | Stability | Confidence | Cost |
|
|
26
|
+
|-------------|-----------|-----------|------------|---------|
|
|
27
|
+
| Unit | Very Fast | Very High | Low-Medium | Low |
|
|
28
|
+
| Integration | Medium | Medium | Medium | Medium |
|
|
29
|
+
| E2E | Slow | Low | High | High |
|
|
30
|
+
|
|
31
|
+
## Unit Tests
|
|
32
|
+
|
|
33
|
+
### Purpose
|
|
34
|
+
|
|
35
|
+
- Test single functions, methods, or classes in isolation
|
|
36
|
+
- Verify business logic correctness
|
|
37
|
+
- Provide fast feedback during development
|
|
38
|
+
- Enable refactoring with confidence
|
|
39
|
+
|
|
40
|
+
### Characteristics
|
|
41
|
+
|
|
42
|
+
- Execute in under 100ms
|
|
43
|
+
- No external dependencies (database, network, filesystem)
|
|
44
|
+
- Deterministic (same input always produces same output)
|
|
45
|
+
- Can run in parallel without interference
|
|
46
|
+
|
|
47
|
+
### Jest Example: Order Total Calculation
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// src/domain/Order.ts
|
|
51
|
+
export class Order {
|
|
52
|
+
private items: OrderItem[] = [];
|
|
53
|
+
|
|
54
|
+
addItem(item: OrderItem): void {
|
|
55
|
+
this.items.push(item);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get subtotal(): number {
|
|
59
|
+
return this.items.reduce(
|
|
60
|
+
(sum, item) => sum + item.price * item.quantity,
|
|
61
|
+
0
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get discount(): number {
|
|
66
|
+
if (this.subtotal >= 100) return this.subtotal * 0.1;
|
|
67
|
+
if (this.subtotal >= 50) return this.subtotal * 0.05;
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get total(): number {
|
|
72
|
+
return this.subtotal - this.discount;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/domain/Order.test.ts
|
|
77
|
+
describe('Order', () => {
|
|
78
|
+
describe('subtotal', () => {
|
|
79
|
+
it('returns zero for empty order', () => {
|
|
80
|
+
const order = new Order();
|
|
81
|
+
|
|
82
|
+
expect(order.subtotal).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('sums item prices multiplied by quantities', () => {
|
|
86
|
+
const order = new Order();
|
|
87
|
+
order.addItem({ sku: 'A', price: 10, quantity: 2 });
|
|
88
|
+
order.addItem({ sku: 'B', price: 25, quantity: 1 });
|
|
89
|
+
|
|
90
|
+
expect(order.subtotal).toBe(45);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('discount', () => {
|
|
95
|
+
it('returns zero when subtotal is under 50', () => {
|
|
96
|
+
const order = new Order();
|
|
97
|
+
order.addItem({ sku: 'A', price: 10, quantity: 4 });
|
|
98
|
+
|
|
99
|
+
expect(order.discount).toBe(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('applies 5% discount when subtotal is 50 or more', () => {
|
|
103
|
+
const order = new Order();
|
|
104
|
+
order.addItem({ sku: 'A', price: 50, quantity: 1 });
|
|
105
|
+
|
|
106
|
+
expect(order.discount).toBe(2.5);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('applies 10% discount when subtotal is 100 or more', () => {
|
|
110
|
+
const order = new Order();
|
|
111
|
+
order.addItem({ sku: 'A', price: 100, quantity: 1 });
|
|
112
|
+
|
|
113
|
+
expect(order.discount).toBe(10);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('total', () => {
|
|
118
|
+
it('returns subtotal minus discount', () => {
|
|
119
|
+
const order = new Order();
|
|
120
|
+
order.addItem({ sku: 'A', price: 50, quantity: 2 });
|
|
121
|
+
|
|
122
|
+
expect(order.total).toBe(90); // 100 - 10% discount
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Pytest Example: Password Validation
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
# src/domain/password_validator.py
|
|
132
|
+
import re
|
|
133
|
+
from dataclasses import dataclass
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class ValidationResult:
|
|
137
|
+
valid: bool
|
|
138
|
+
errors: list[str]
|
|
139
|
+
|
|
140
|
+
class PasswordValidator:
|
|
141
|
+
MIN_LENGTH = 8
|
|
142
|
+
MAX_LENGTH = 128
|
|
143
|
+
|
|
144
|
+
def validate(self, password: str) -> ValidationResult:
|
|
145
|
+
errors = []
|
|
146
|
+
|
|
147
|
+
if len(password) < self.MIN_LENGTH:
|
|
148
|
+
errors.append(f"Password must be at least {self.MIN_LENGTH} characters")
|
|
149
|
+
|
|
150
|
+
if len(password) > self.MAX_LENGTH:
|
|
151
|
+
errors.append(f"Password must not exceed {self.MAX_LENGTH} characters")
|
|
152
|
+
|
|
153
|
+
if not re.search(r'[A-Z]', password):
|
|
154
|
+
errors.append("Password must contain at least one uppercase letter")
|
|
155
|
+
|
|
156
|
+
if not re.search(r'[a-z]', password):
|
|
157
|
+
errors.append("Password must contain at least one lowercase letter")
|
|
158
|
+
|
|
159
|
+
if not re.search(r'\d', password):
|
|
160
|
+
errors.append("Password must contain at least one digit")
|
|
161
|
+
|
|
162
|
+
if not re.search(r'[!@#$%^&*]', password):
|
|
163
|
+
errors.append("Password must contain at least one special character")
|
|
164
|
+
|
|
165
|
+
return ValidationResult(valid=len(errors) == 0, errors=errors)
|
|
166
|
+
|
|
167
|
+
# tests/domain/test_password_validator.py
|
|
168
|
+
import pytest
|
|
169
|
+
from src.domain.password_validator import PasswordValidator, ValidationResult
|
|
170
|
+
|
|
171
|
+
class TestPasswordValidator:
|
|
172
|
+
@pytest.fixture
|
|
173
|
+
def validator(self):
|
|
174
|
+
return PasswordValidator()
|
|
175
|
+
|
|
176
|
+
def test_accepts_valid_password(self, validator):
|
|
177
|
+
result = validator.validate("SecurePass1!")
|
|
178
|
+
|
|
179
|
+
assert result.valid is True
|
|
180
|
+
assert result.errors == []
|
|
181
|
+
|
|
182
|
+
@pytest.mark.parametrize("password,expected_error", [
|
|
183
|
+
("Short1!", "Password must be at least 8 characters"),
|
|
184
|
+
("a" * 129 + "A1!", "Password must not exceed 128 characters"),
|
|
185
|
+
("lowercase1!", "Password must contain at least one uppercase letter"),
|
|
186
|
+
("UPPERCASE1!", "Password must contain at least one lowercase letter"),
|
|
187
|
+
("NoDigits!", "Password must contain at least one digit"),
|
|
188
|
+
("NoSpecial1", "Password must contain at least one special character"),
|
|
189
|
+
])
|
|
190
|
+
def test_rejects_invalid_passwords(self, validator, password, expected_error):
|
|
191
|
+
result = validator.validate(password)
|
|
192
|
+
|
|
193
|
+
assert result.valid is False
|
|
194
|
+
assert expected_error in result.errors
|
|
195
|
+
|
|
196
|
+
def test_collects_multiple_validation_errors(self, validator):
|
|
197
|
+
result = validator.validate("bad")
|
|
198
|
+
|
|
199
|
+
assert result.valid is False
|
|
200
|
+
assert len(result.errors) == 5 # Too short + missing uppercase + lowercase + digit + special
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Integration Tests
|
|
204
|
+
|
|
205
|
+
### Purpose
|
|
206
|
+
|
|
207
|
+
- Verify interactions between components
|
|
208
|
+
- Test database operations and queries
|
|
209
|
+
- Validate API contracts and responses
|
|
210
|
+
- Ensure external service integrations work correctly
|
|
211
|
+
|
|
212
|
+
### Characteristics
|
|
213
|
+
|
|
214
|
+
- Execute in 1-5 seconds
|
|
215
|
+
- May use real databases (often in-memory or containers)
|
|
216
|
+
- Test multiple units working together
|
|
217
|
+
- Verify data persistence and retrieval
|
|
218
|
+
|
|
219
|
+
### Jest Example: User Repository with Database
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// tests/integration/repositories/UserRepository.test.ts
|
|
223
|
+
import { UserRepository } from '@/infrastructure/UserRepository';
|
|
224
|
+
import { PrismaClient } from '@prisma/client';
|
|
225
|
+
import { execSync } from 'child_process';
|
|
226
|
+
|
|
227
|
+
describe('UserRepository', () => {
|
|
228
|
+
let prisma: PrismaClient;
|
|
229
|
+
let repository: UserRepository;
|
|
230
|
+
|
|
231
|
+
beforeAll(async () => {
|
|
232
|
+
// Use test database
|
|
233
|
+
process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/test_db';
|
|
234
|
+
prisma = new PrismaClient();
|
|
235
|
+
await prisma.$connect();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
afterAll(async () => {
|
|
239
|
+
await prisma.$disconnect();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
beforeEach(async () => {
|
|
243
|
+
// Clean database before each test
|
|
244
|
+
await prisma.user.deleteMany();
|
|
245
|
+
repository = new UserRepository(prisma);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('create', () => {
|
|
249
|
+
it('persists user to database and returns with generated id', async () => {
|
|
250
|
+
const userData = {
|
|
251
|
+
email: 'test@example.com',
|
|
252
|
+
name: 'Test User',
|
|
253
|
+
hashedPassword: 'hashed123',
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const created = await repository.create(userData);
|
|
257
|
+
|
|
258
|
+
expect(created.id).toBeDefined();
|
|
259
|
+
expect(created.email).toBe(userData.email);
|
|
260
|
+
|
|
261
|
+
// Verify persistence
|
|
262
|
+
const found = await prisma.user.findUnique({
|
|
263
|
+
where: { id: created.id },
|
|
264
|
+
});
|
|
265
|
+
expect(found).not.toBeNull();
|
|
266
|
+
expect(found?.email).toBe(userData.email);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('throws ConflictError when email already exists', async () => {
|
|
270
|
+
const userData = {
|
|
271
|
+
email: 'duplicate@example.com',
|
|
272
|
+
name: 'First User',
|
|
273
|
+
hashedPassword: 'hashed123',
|
|
274
|
+
};
|
|
275
|
+
await repository.create(userData);
|
|
276
|
+
|
|
277
|
+
await expect(repository.create({
|
|
278
|
+
...userData,
|
|
279
|
+
name: 'Second User',
|
|
280
|
+
})).rejects.toThrow('User with this email already exists');
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('findByEmail', () => {
|
|
285
|
+
it('returns user when found', async () => {
|
|
286
|
+
const created = await repository.create({
|
|
287
|
+
email: 'findme@example.com',
|
|
288
|
+
name: 'Find Me',
|
|
289
|
+
hashedPassword: 'hashed123',
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const found = await repository.findByEmail('findme@example.com');
|
|
293
|
+
|
|
294
|
+
expect(found).toEqual(created);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('returns null when not found', async () => {
|
|
298
|
+
const found = await repository.findByEmail('nonexistent@example.com');
|
|
299
|
+
|
|
300
|
+
expect(found).toBeNull();
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Pytest Example: API Integration Tests
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
# tests/integration/api/test_users_api.py
|
|
310
|
+
import pytest
|
|
311
|
+
from fastapi.testclient import TestClient
|
|
312
|
+
from sqlalchemy import create_engine
|
|
313
|
+
from sqlalchemy.orm import sessionmaker
|
|
314
|
+
|
|
315
|
+
from src.main import app
|
|
316
|
+
from src.database import Base, get_db
|
|
317
|
+
|
|
318
|
+
# Test database setup
|
|
319
|
+
TEST_DATABASE_URL = "sqlite:///./test.db"
|
|
320
|
+
engine = create_engine(TEST_DATABASE_URL, connect_args={"check_same_thread": False})
|
|
321
|
+
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
322
|
+
|
|
323
|
+
@pytest.fixture(scope="function")
|
|
324
|
+
def db_session():
|
|
325
|
+
Base.metadata.create_all(bind=engine)
|
|
326
|
+
session = TestingSessionLocal()
|
|
327
|
+
try:
|
|
328
|
+
yield session
|
|
329
|
+
finally:
|
|
330
|
+
session.close()
|
|
331
|
+
Base.metadata.drop_all(bind=engine)
|
|
332
|
+
|
|
333
|
+
@pytest.fixture(scope="function")
|
|
334
|
+
def client(db_session):
|
|
335
|
+
def override_get_db():
|
|
336
|
+
yield db_session
|
|
337
|
+
|
|
338
|
+
app.dependency_overrides[get_db] = override_get_db
|
|
339
|
+
with TestClient(app) as test_client:
|
|
340
|
+
yield test_client
|
|
341
|
+
app.dependency_overrides.clear()
|
|
342
|
+
|
|
343
|
+
class TestUsersAPI:
|
|
344
|
+
def test_create_user_returns_201_with_user_data(self, client):
|
|
345
|
+
response = client.post("/api/users", json={
|
|
346
|
+
"email": "new@user.com",
|
|
347
|
+
"password": "SecurePass1!",
|
|
348
|
+
"name": "New User"
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
assert response.status_code == 201
|
|
352
|
+
data = response.json()
|
|
353
|
+
assert data["email"] == "new@user.com"
|
|
354
|
+
assert data["name"] == "New User"
|
|
355
|
+
assert "id" in data
|
|
356
|
+
assert "password" not in data # Should not expose password
|
|
357
|
+
|
|
358
|
+
def test_create_user_returns_400_for_invalid_email(self, client):
|
|
359
|
+
response = client.post("/api/users", json={
|
|
360
|
+
"email": "invalid-email",
|
|
361
|
+
"password": "SecurePass1!",
|
|
362
|
+
"name": "New User"
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
assert response.status_code == 400
|
|
366
|
+
assert "email" in response.json()["detail"].lower()
|
|
367
|
+
|
|
368
|
+
def test_create_user_returns_409_for_duplicate_email(self, client):
|
|
369
|
+
user_data = {
|
|
370
|
+
"email": "duplicate@user.com",
|
|
371
|
+
"password": "SecurePass1!",
|
|
372
|
+
"name": "First User"
|
|
373
|
+
}
|
|
374
|
+
client.post("/api/users", json=user_data)
|
|
375
|
+
|
|
376
|
+
response = client.post("/api/users", json=user_data)
|
|
377
|
+
|
|
378
|
+
assert response.status_code == 409
|
|
379
|
+
|
|
380
|
+
def test_get_user_returns_200_when_found(self, client):
|
|
381
|
+
create_response = client.post("/api/users", json={
|
|
382
|
+
"email": "existing@user.com",
|
|
383
|
+
"password": "SecurePass1!",
|
|
384
|
+
"name": "Existing User"
|
|
385
|
+
})
|
|
386
|
+
user_id = create_response.json()["id"]
|
|
387
|
+
|
|
388
|
+
response = client.get(f"/api/users/{user_id}")
|
|
389
|
+
|
|
390
|
+
assert response.status_code == 200
|
|
391
|
+
assert response.json()["email"] == "existing@user.com"
|
|
392
|
+
|
|
393
|
+
def test_get_user_returns_404_when_not_found(self, client):
|
|
394
|
+
response = client.get("/api/users/nonexistent-id")
|
|
395
|
+
|
|
396
|
+
assert response.status_code == 404
|
|
397
|
+
|
|
398
|
+
def test_list_users_returns_paginated_results(self, client):
|
|
399
|
+
# Create 15 users
|
|
400
|
+
for i in range(15):
|
|
401
|
+
client.post("/api/users", json={
|
|
402
|
+
"email": f"user{i}@example.com",
|
|
403
|
+
"password": "SecurePass1!",
|
|
404
|
+
"name": f"User {i}"
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
response = client.get("/api/users?page=1&limit=10")
|
|
408
|
+
|
|
409
|
+
assert response.status_code == 200
|
|
410
|
+
data = response.json()
|
|
411
|
+
assert len(data["items"]) == 10
|
|
412
|
+
assert data["total"] == 15
|
|
413
|
+
assert data["page"] == 1
|
|
414
|
+
assert data["pages"] == 2
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
## E2E Tests
|
|
418
|
+
|
|
419
|
+
### Purpose
|
|
420
|
+
|
|
421
|
+
- Validate critical user journeys through the entire system
|
|
422
|
+
- Ensure the application works from the user's perspective
|
|
423
|
+
- Catch integration issues that other tests miss
|
|
424
|
+
- Verify deployment and infrastructure configuration
|
|
425
|
+
|
|
426
|
+
### Characteristics
|
|
427
|
+
|
|
428
|
+
- Execute in 10-60 seconds
|
|
429
|
+
- Run against deployed application (staging or local)
|
|
430
|
+
- Use real browser or API client
|
|
431
|
+
- Test complete user workflows
|
|
432
|
+
|
|
433
|
+
### Playwright Example: User Registration Flow
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
// tests/e2e/user-registration.spec.ts
|
|
437
|
+
import { test, expect } from '@playwright/test';
|
|
438
|
+
|
|
439
|
+
test.describe('User Registration', () => {
|
|
440
|
+
test.beforeEach(async ({ page }) => {
|
|
441
|
+
await page.goto('/register');
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test('allows new user to register and login', async ({ page }) => {
|
|
445
|
+
const uniqueEmail = `test-${Date.now()}@example.com`;
|
|
446
|
+
|
|
447
|
+
// Fill registration form
|
|
448
|
+
await page.getByLabel('Email').fill(uniqueEmail);
|
|
449
|
+
await page.getByLabel('Password').fill('SecurePass1!');
|
|
450
|
+
await page.getByLabel('Confirm Password').fill('SecurePass1!');
|
|
451
|
+
await page.getByLabel('Full Name').fill('Test User');
|
|
452
|
+
await page.getByRole('button', { name: 'Create Account' }).click();
|
|
453
|
+
|
|
454
|
+
// Verify redirect to login with success message
|
|
455
|
+
await expect(page).toHaveURL('/login');
|
|
456
|
+
await expect(page.getByText('Account created successfully')).toBeVisible();
|
|
457
|
+
|
|
458
|
+
// Login with new credentials
|
|
459
|
+
await page.getByLabel('Email').fill(uniqueEmail);
|
|
460
|
+
await page.getByLabel('Password').fill('SecurePass1!');
|
|
461
|
+
await page.getByRole('button', { name: 'Sign In' }).click();
|
|
462
|
+
|
|
463
|
+
// Verify logged in state
|
|
464
|
+
await expect(page).toHaveURL('/dashboard');
|
|
465
|
+
await expect(page.getByText('Welcome, Test User')).toBeVisible();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test('shows validation errors for invalid input', async ({ page }) => {
|
|
469
|
+
// Submit with empty form
|
|
470
|
+
await page.getByRole('button', { name: 'Create Account' }).click();
|
|
471
|
+
|
|
472
|
+
// Verify validation messages
|
|
473
|
+
await expect(page.getByText('Email is required')).toBeVisible();
|
|
474
|
+
await expect(page.getByText('Password is required')).toBeVisible();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test('prevents registration with existing email', async ({ page }) => {
|
|
478
|
+
// Attempt to register with known existing email
|
|
479
|
+
await page.getByLabel('Email').fill('existing@user.com');
|
|
480
|
+
await page.getByLabel('Password').fill('SecurePass1!');
|
|
481
|
+
await page.getByLabel('Confirm Password').fill('SecurePass1!');
|
|
482
|
+
await page.getByLabel('Full Name').fill('Another User');
|
|
483
|
+
await page.getByRole('button', { name: 'Create Account' }).click();
|
|
484
|
+
|
|
485
|
+
// Verify error message
|
|
486
|
+
await expect(page.getByText('An account with this email already exists')).toBeVisible();
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Cypress Example: E-commerce Checkout
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
// cypress/e2e/checkout.cy.ts
|
|
495
|
+
describe('Checkout Flow', () => {
|
|
496
|
+
beforeEach(() => {
|
|
497
|
+
cy.loginAs('test@customer.com');
|
|
498
|
+
cy.clearCart();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('completes purchase with valid payment', () => {
|
|
502
|
+
// Add items to cart
|
|
503
|
+
cy.visit('/products/widget-pro');
|
|
504
|
+
cy.get('[data-testid="add-to-cart"]').click();
|
|
505
|
+
cy.get('[data-testid="cart-count"]').should('contain', '1');
|
|
506
|
+
|
|
507
|
+
// Proceed to checkout
|
|
508
|
+
cy.visit('/cart');
|
|
509
|
+
cy.get('[data-testid="checkout-button"]').click();
|
|
510
|
+
|
|
511
|
+
// Fill shipping address
|
|
512
|
+
cy.get('#shipping-address').type('123 Test Street');
|
|
513
|
+
cy.get('#shipping-city').type('Test City');
|
|
514
|
+
cy.get('#shipping-zip').type('12345');
|
|
515
|
+
cy.get('[data-testid="continue-to-payment"]').click();
|
|
516
|
+
|
|
517
|
+
// Fill payment (test card)
|
|
518
|
+
cy.getStripeElement('cardNumber').type('4242424242424242');
|
|
519
|
+
cy.getStripeElement('cardExpiry').type('1225');
|
|
520
|
+
cy.getStripeElement('cardCvc').type('123');
|
|
521
|
+
cy.get('[data-testid="place-order"]').click();
|
|
522
|
+
|
|
523
|
+
// Verify success
|
|
524
|
+
cy.url().should('include', '/order-confirmation');
|
|
525
|
+
cy.get('[data-testid="order-number"]').should('exist');
|
|
526
|
+
cy.get('[data-testid="order-total"]').should('contain', '$49.99');
|
|
527
|
+
|
|
528
|
+
// Verify email sent (via test mailbox API)
|
|
529
|
+
cy.task('getLastEmail', 'test@customer.com').then((email) => {
|
|
530
|
+
expect(email.subject).to.include('Order Confirmation');
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('handles payment failure gracefully', () => {
|
|
535
|
+
cy.visit('/products/widget-pro');
|
|
536
|
+
cy.get('[data-testid="add-to-cart"]').click();
|
|
537
|
+
cy.visit('/cart');
|
|
538
|
+
cy.get('[data-testid="checkout-button"]').click();
|
|
539
|
+
|
|
540
|
+
// Fill shipping
|
|
541
|
+
cy.get('#shipping-address').type('123 Test Street');
|
|
542
|
+
cy.get('#shipping-city').type('Test City');
|
|
543
|
+
cy.get('#shipping-zip').type('12345');
|
|
544
|
+
cy.get('[data-testid="continue-to-payment"]').click();
|
|
545
|
+
|
|
546
|
+
// Use declining test card
|
|
547
|
+
cy.getStripeElement('cardNumber').type('4000000000000002');
|
|
548
|
+
cy.getStripeElement('cardExpiry').type('1225');
|
|
549
|
+
cy.getStripeElement('cardCvc').type('123');
|
|
550
|
+
cy.get('[data-testid="place-order"]').click();
|
|
551
|
+
|
|
552
|
+
// Verify error handling
|
|
553
|
+
cy.get('[data-testid="payment-error"]')
|
|
554
|
+
.should('contain', 'Your card was declined');
|
|
555
|
+
cy.url().should('include', '/checkout/payment');
|
|
556
|
+
cy.get('[data-testid="cart-count"]').should('contain', '1');
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
## Test Selection Guidelines
|
|
562
|
+
|
|
563
|
+
### When to Write Unit Tests
|
|
564
|
+
|
|
565
|
+
- Pure functions with business logic
|
|
566
|
+
- Data transformations and calculations
|
|
567
|
+
- Validation and parsing logic
|
|
568
|
+
- State machines and reducers
|
|
569
|
+
- Utility and helper functions
|
|
570
|
+
|
|
571
|
+
### When to Write Integration Tests
|
|
572
|
+
|
|
573
|
+
- Database queries and transactions
|
|
574
|
+
- API endpoints and middleware
|
|
575
|
+
- Service-to-service communication
|
|
576
|
+
- Message queue producers/consumers
|
|
577
|
+
- Cache operations
|
|
578
|
+
|
|
579
|
+
### When to Write E2E Tests
|
|
580
|
+
|
|
581
|
+
- User registration and authentication
|
|
582
|
+
- Payment and checkout flows
|
|
583
|
+
- Critical business workflows
|
|
584
|
+
- Multi-step forms and wizards
|
|
585
|
+
- Features involving multiple services
|
|
586
|
+
|
|
587
|
+
## Variations
|
|
588
|
+
|
|
589
|
+
### Contract Testing (Alternative to E2E)
|
|
590
|
+
|
|
591
|
+
For microservices, consider contract tests instead of E2E:
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
// consumer/tests/UserServiceContract.test.ts
|
|
595
|
+
import { Pact } from '@pact-foundation/pact';
|
|
596
|
+
|
|
597
|
+
const provider = new Pact({
|
|
598
|
+
consumer: 'OrderService',
|
|
599
|
+
provider: 'UserService',
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
describe('UserService Contract', () => {
|
|
603
|
+
it('returns user details for valid user id', async () => {
|
|
604
|
+
await provider.addInteraction({
|
|
605
|
+
state: 'user 123 exists',
|
|
606
|
+
uponReceiving: 'a request for user 123',
|
|
607
|
+
withRequest: {
|
|
608
|
+
method: 'GET',
|
|
609
|
+
path: '/users/123',
|
|
610
|
+
},
|
|
611
|
+
willRespondWith: {
|
|
612
|
+
status: 200,
|
|
613
|
+
body: {
|
|
614
|
+
id: '123',
|
|
615
|
+
email: Matchers.email(),
|
|
616
|
+
name: Matchers.string(),
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
const client = new UserServiceClient(provider.mockService.baseUrl);
|
|
622
|
+
const user = await client.getUser('123');
|
|
623
|
+
|
|
624
|
+
expect(user.id).toBe('123');
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### Visual Regression Testing
|
|
630
|
+
|
|
631
|
+
For UI-heavy applications:
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
// tests/visual/Button.visual.test.ts
|
|
635
|
+
import { test, expect } from '@playwright/test';
|
|
636
|
+
|
|
637
|
+
test.describe('Button Visual Regression', () => {
|
|
638
|
+
test('primary button states', async ({ page }) => {
|
|
639
|
+
await page.goto('/storybook/button--primary');
|
|
640
|
+
|
|
641
|
+
// Default state
|
|
642
|
+
await expect(page.locator('.button')).toHaveScreenshot('button-default.png');
|
|
643
|
+
|
|
644
|
+
// Hover state
|
|
645
|
+
await page.locator('.button').hover();
|
|
646
|
+
await expect(page.locator('.button')).toHaveScreenshot('button-hover.png');
|
|
647
|
+
|
|
648
|
+
// Focus state
|
|
649
|
+
await page.locator('.button').focus();
|
|
650
|
+
await expect(page.locator('.button')).toHaveScreenshot('button-focus.png');
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
## Anti-Patterns
|
|
656
|
+
|
|
657
|
+
### Testing at Wrong Level
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
// WRONG: Using E2E test for simple calculation
|
|
661
|
+
test('calculates discount correctly', async ({ page }) => {
|
|
662
|
+
await page.goto('/cart');
|
|
663
|
+
await page.fill('#quantity', '10');
|
|
664
|
+
await expect(page.locator('#discount')).toHaveText('10%');
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// CORRECT: Unit test for calculation logic
|
|
668
|
+
test('applies 10% discount for orders of 10+ items', () => {
|
|
669
|
+
expect(calculateDiscount(10)).toBe(0.1);
|
|
670
|
+
});
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Missing Critical Paths
|
|
674
|
+
|
|
675
|
+
```
|
|
676
|
+
// WRONG: Skipping E2E for checkout
|
|
677
|
+
Unit tests: 95% coverage
|
|
678
|
+
Integration tests: API endpoints tested
|
|
679
|
+
E2E tests: None for payment flow
|
|
680
|
+
|
|
681
|
+
// Payment bugs slip through because the complete flow
|
|
682
|
+
// (UI -> API -> Payment Provider -> Webhook -> DB) is never tested
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### Excessive E2E Tests
|
|
686
|
+
|
|
687
|
+
```
|
|
688
|
+
// WRONG: E2E test for every validation rule
|
|
689
|
+
e2e/
|
|
690
|
+
registration-empty-email.spec.ts
|
|
691
|
+
registration-invalid-email.spec.ts
|
|
692
|
+
registration-short-password.spec.ts
|
|
693
|
+
registration-no-uppercase.spec.ts
|
|
694
|
+
registration-no-number.spec.ts
|
|
695
|
+
... 20 more files
|
|
696
|
+
|
|
697
|
+
// CORRECT: Unit tests for validation, single E2E for happy path
|
|
698
|
+
unit/
|
|
699
|
+
PasswordValidator.test.ts // All validation rules
|
|
700
|
+
e2e/
|
|
701
|
+
registration.spec.ts // One happy path + one error case
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### Ignoring Test Pyramid Distribution
|
|
705
|
+
|
|
706
|
+
```
|
|
707
|
+
// WRONG: Inverted pyramid
|
|
708
|
+
E2E Tests: 500 (slow, flaky)
|
|
709
|
+
Integration Tests: 50
|
|
710
|
+
Unit Tests: 100
|
|
711
|
+
|
|
712
|
+
// Build time: 45 minutes
|
|
713
|
+
// Flaky test rate: 15%
|
|
714
|
+
// Developer confidence: Low
|
|
715
|
+
|
|
716
|
+
// CORRECT: Following pyramid
|
|
717
|
+
Unit Tests: 2000 (fast, stable)
|
|
718
|
+
Integration Tests: 300
|
|
719
|
+
E2E Tests: 50
|
|
720
|
+
|
|
721
|
+
// Build time: 8 minutes
|
|
722
|
+
// Flaky test rate: 1%
|
|
723
|
+
// Developer confidence: High
|
|
724
|
+
```
|