@comfanion/workflow 4.38.4-dev.1 → 4.39.0-dev.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/package.json +1 -1
- package/src/build-info.json +2 -2
- package/src/opencode/skills/acceptance-criteria/SKILL.md +58 -176
- package/src/opencode/skills/architecture-design/SKILL.md +86 -576
- package/src/opencode/skills/archiving/SKILL.md +60 -140
- package/src/opencode/skills/coding-standards/SKILL.md +113 -434
- package/src/opencode/skills/coding-standards/what-to-document.md +512 -0
- package/src/opencode/skills/database-design/SKILL.md +94 -778
- package/src/opencode/skills/database-design/indexing.md +187 -0
- package/src/opencode/skills/database-design/migrations.md +239 -0
- package/src/opencode/skills/database-design/schema-design.md +319 -0
- package/src/opencode/skills/doc-todo/SKILL.md +35 -27
- package/src/opencode/skills/epic-writing/SKILL.md +156 -244
- package/src/opencode/skills/epic-writing/template.md +11 -1
- package/src/opencode/skills/methodologies/SKILL.md +91 -354
- package/src/opencode/skills/methodologies/define.md +336 -0
- package/src/opencode/skills/methodologies/diagnose.md +374 -0
- package/src/opencode/skills/methodologies/empathize.md +253 -0
- package/src/opencode/skills/methodologies/ideate.md +458 -0
- package/src/opencode/skills/prd-writing/SKILL.md +162 -366
- package/src/opencode/skills/prd-writing/template.md +178 -48
- package/src/opencode/skills/requirements-gathering/SKILL.md +102 -117
- package/src/opencode/skills/requirements-gathering/template.md +97 -17
- package/src/opencode/skills/sprint-planning/SKILL.md +76 -225
- package/src/opencode/skills/sprint-planning/template.yaml +8 -0
- package/src/opencode/skills/story-writing/SKILL.md +76 -210
- package/src/opencode/skills/story-writing/template.md +10 -1
- package/src/opencode/skills/test-design/SKILL.md +78 -84
- package/src/opencode/skills/test-design/test-strategy.md +279 -0
- package/src/opencode/skills/test-design/unit-tests-mocking.md +247 -0
- package/src/opencode/skills/test-design/unit-tests-patterns.md +181 -0
- package/src/opencode/skills/test-design/unit-tests.md +117 -0
- package/src/opencode/skills/unit-writing/SKILL.md +119 -377
- package/src/opencode/skills/module-documentation/SKILL.md +0 -224
- package/src/opencode/skills/module-documentation/template.md +0 -139
- /package/src/opencode/skills/test-design/{template-integration.md → templates/template-integration.md} +0 -0
- /package/src/opencode/skills/test-design/{template-module.md → templates/template-module.md} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: test-design
|
|
3
|
-
description: Use when designing test strategy,
|
|
3
|
+
description: Design test strategy, write unit/integration/E2E tests, plan test coverage, and define testing approach. Use when designing tests, writing test cases, planning test coverage, or when user mentions "test design", "test strategy", "unit tests", "integration tests", "test coverage", or "testing approach".
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: opencode
|
|
6
6
|
metadata:
|
|
@@ -10,102 +10,96 @@ metadata:
|
|
|
10
10
|
|
|
11
11
|
# Test Design Skill
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
```xml
|
|
14
|
+
<test_design>
|
|
15
|
+
<definition>Design test strategy, write unit/integration/E2E tests</definition>
|
|
16
|
+
|
|
17
|
+
<test_types>
|
|
18
|
+
<unit>Individual components, fast, isolated → See [unit-tests.md](unit-tests.md)</unit>
|
|
19
|
+
<integration>Module contracts, API boundaries → See [integration-tests.md](integration-tests.md)</integration>
|
|
20
|
+
<e2e>User scenarios, critical paths → See [e2e-tests.md](e2e-tests.md)</e2e>
|
|
21
|
+
</test_types>
|
|
22
|
+
|
|
23
|
+
<test_pyramid>
|
|
24
|
+
<many>Unit tests (70-80% coverage)</many>
|
|
25
|
+
<some>Integration tests (50-60% coverage)</some>
|
|
26
|
+
<few>E2E tests (20-30% critical paths)</few>
|
|
27
|
+
</test_pyramid>
|
|
28
|
+
|
|
29
|
+
<quick_reference>
|
|
30
|
+
<what_to_test>Public API, Business logic, Error handling, Edge cases</what_to_test>
|
|
31
|
+
<what_not_to_test>Private methods, Getters/setters, Framework internals</what_not_to_test>
|
|
32
|
+
<naming>Test{Component}_{Method}_{Scenario}_{Expected}</naming>
|
|
33
|
+
<structure>AAA: Arrange → Act → Assert</structure>
|
|
34
|
+
</quick_reference>
|
|
35
|
+
|
|
36
|
+
<coverage_targets>
|
|
37
|
+
<domain>80%+</domain>
|
|
38
|
+
<application>70%+</application>
|
|
39
|
+
<infrastructure>50%+</infrastructure>
|
|
40
|
+
<focus>Critical paths, not 100%</focus>
|
|
41
|
+
</coverage_targets>
|
|
42
|
+
|
|
43
|
+
<when_to_test>
|
|
44
|
+
<tdd>Red → Green → Refactor (clear requirements)</tdd>
|
|
45
|
+
<after>Write code first (prototyping, unclear requirements)</after>
|
|
46
|
+
</when_to_test>
|
|
47
|
+
</test_design>
|
|
17
48
|
```
|
|
18
|
-
Test{Component}_{Method}_{Scenario}_{Expected}
|
|
19
|
-
|
|
20
|
-
Examples:
|
|
21
|
-
- TestUser_Create_ValidData_Success
|
|
22
|
-
- TestUser_Create_EmptyEmail_ReturnsError
|
|
23
|
-
- TestOrder_Cancel_AlreadyShipped_Fails
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Test Structure (AAA)
|
|
27
49
|
|
|
28
|
-
|
|
29
|
-
// Arrange - Setup
|
|
30
|
-
input := ...
|
|
31
|
-
expected := ...
|
|
50
|
+
---
|
|
32
51
|
|
|
33
|
-
|
|
34
|
-
result := sut.Method(input)
|
|
52
|
+
## Detailed Guides
|
|
35
53
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
**Unit Testing:**
|
|
55
|
+
- [unit-tests.md](unit-tests.md) - Basics: what to test, AAA pattern, naming
|
|
56
|
+
- [unit-tests-patterns.md](unit-tests-patterns.md) - Table-driven, state transitions, validation
|
|
57
|
+
- [unit-tests-mocking.md](unit-tests-mocking.md) - Mocking, DI, test isolation
|
|
39
58
|
|
|
40
|
-
|
|
59
|
+
**Integration Testing:**
|
|
60
|
+
- [integration-tests.md](integration-tests.md) - Module contracts, API boundaries, events
|
|
41
61
|
|
|
42
|
-
|
|
62
|
+
**E2E Testing:**
|
|
63
|
+
- [e2e-tests.md](e2e-tests.md) - User scenarios, critical paths, smoke tests
|
|
43
64
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
name string
|
|
47
|
-
input Input
|
|
48
|
-
expected Output
|
|
49
|
-
wantErr bool
|
|
50
|
-
}{
|
|
51
|
-
{"valid input", validInput(), expectedOutput(), false},
|
|
52
|
-
{"empty input", emptyInput(), nil, true},
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
for _, tt := range tests {
|
|
56
|
-
t.Run(tt.name, func(t *testing.T) {
|
|
57
|
-
result, err := sut.Method(tt.input)
|
|
58
|
-
// assert
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
```
|
|
65
|
+
**Strategy:**
|
|
66
|
+
- [test-strategy.md](test-strategy.md) - Test pyramid, coverage targets, when to use what
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
**Templates:**
|
|
69
|
+
- [templates/template-integration.md](templates/template-integration.md) - Architecture integration tests
|
|
70
|
+
- [templates/template-module.md](templates/template-module.md) - Module test cases
|
|
64
71
|
|
|
65
|
-
|
|
66
|
-
- Public API / exported functions
|
|
67
|
-
- Business logic / domain rules
|
|
68
|
-
- Error handling paths
|
|
69
|
-
- Edge cases (empty, nil, zero, max)
|
|
70
|
-
- State transitions
|
|
72
|
+
---
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
## Quick Example
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// Unit test
|
|
78
|
+
it('calculates order total', () => {
|
|
79
|
+
const order = new Order();
|
|
80
|
+
order.addItem({ price: 100, quantity: 2 });
|
|
81
|
+
expect(order.calculateTotal()).toBe(200);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Integration test
|
|
85
|
+
it('saves order to database', async () => {
|
|
86
|
+
const order = await orderService.create(orderData);
|
|
87
|
+
const saved = await db.orders.findById(order.id);
|
|
88
|
+
expect(saved).toMatchObject(orderData);
|
|
89
|
+
});
|
|
90
|
+
```
|
|
77
91
|
|
|
78
|
-
|
|
92
|
+
---
|
|
79
93
|
|
|
80
|
-
|
|
81
|
-
- External services (HTTP, DB)
|
|
82
|
-
- Non-deterministic (time, random)
|
|
83
|
-
- Slow operations
|
|
94
|
+
## Test Pyramid
|
|
84
95
|
|
|
85
|
-
Prefer interfaces for testability:
|
|
86
96
|
```
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
/\
|
|
98
|
+
/ \ E2E (Few)
|
|
99
|
+
/____\
|
|
100
|
+
/ \ Integration (Some)
|
|
101
|
+
/ \
|
|
102
|
+
/__________\ Unit (Many)
|
|
93
103
|
```
|
|
94
104
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
Each test should:
|
|
98
|
-
- Setup its own data
|
|
99
|
-
- Not depend on other tests
|
|
100
|
-
- Clean up after itself
|
|
101
|
-
- Run in any order
|
|
102
|
-
|
|
103
|
-
## Coverage Guidelines
|
|
104
|
-
|
|
105
|
-
| Layer | Target |
|
|
106
|
-
|-------|--------|
|
|
107
|
-
| Domain/Business | 80%+ |
|
|
108
|
-
| Application/UseCase | 70%+ |
|
|
109
|
-
| Infrastructure | 50%+ |
|
|
110
|
-
|
|
111
|
-
Focus on critical paths, not 100% coverage.
|
|
105
|
+
For full details, see the guides above.
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Test Strategy
|
|
2
|
+
|
|
3
|
+
When to use which type of test and how much coverage.
|
|
4
|
+
|
|
5
|
+
## Test Pyramid
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
/\
|
|
9
|
+
/ \ E2E (Few)
|
|
10
|
+
/____\ - User scenarios
|
|
11
|
+
/ \ - Critical paths
|
|
12
|
+
/ \
|
|
13
|
+
/__________\ Integration (Some)
|
|
14
|
+
/ \ - Module contracts
|
|
15
|
+
/ \- API boundaries
|
|
16
|
+
/________________\ Unit (Many)
|
|
17
|
+
- Business logic
|
|
18
|
+
- Fast, isolated
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Principle:** More unit tests, fewer E2E tests.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Test Types
|
|
26
|
+
|
|
27
|
+
### Unit Tests
|
|
28
|
+
**What:** Individual components in isolation
|
|
29
|
+
**When:** Business logic, calculations, validations
|
|
30
|
+
**Speed:** Fast (milliseconds)
|
|
31
|
+
**Coverage:** 70-80%
|
|
32
|
+
|
|
33
|
+
**Example:**
|
|
34
|
+
```typescript
|
|
35
|
+
it('calculates order total', () => {
|
|
36
|
+
const order = new Order();
|
|
37
|
+
order.addItem({ price: 100, quantity: 2 });
|
|
38
|
+
expect(order.calculateTotal()).toBe(200);
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Integration Tests
|
|
43
|
+
**What:** Multiple components working together
|
|
44
|
+
**When:** Module boundaries, API contracts, DB operations
|
|
45
|
+
**Speed:** Medium (seconds)
|
|
46
|
+
**Coverage:** 50-60%
|
|
47
|
+
|
|
48
|
+
**Example:**
|
|
49
|
+
```typescript
|
|
50
|
+
it('creates order in database', async () => {
|
|
51
|
+
const order = await orderService.create(orderData);
|
|
52
|
+
const saved = await db.orders.findById(order.id);
|
|
53
|
+
expect(saved).toMatchObject(orderData);
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### E2E Tests
|
|
58
|
+
**What:** Complete user scenarios
|
|
59
|
+
**When:** Critical user flows, smoke tests
|
|
60
|
+
**Speed:** Slow (minutes)
|
|
61
|
+
**Coverage:** 20-30% of critical paths
|
|
62
|
+
|
|
63
|
+
**Example:**
|
|
64
|
+
```typescript
|
|
65
|
+
it('user can place order', async () => {
|
|
66
|
+
await page.goto('/products');
|
|
67
|
+
await page.click('[data-testid="add-to-cart"]');
|
|
68
|
+
await page.click('[data-testid="checkout"]');
|
|
69
|
+
await page.fill('[name="email"]', 'test@example.com');
|
|
70
|
+
await page.click('[data-testid="place-order"]');
|
|
71
|
+
await expect(page.locator('.success')).toBeVisible();
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Coverage Targets
|
|
78
|
+
|
|
79
|
+
### By Layer
|
|
80
|
+
|
|
81
|
+
| Layer | Target | Focus |
|
|
82
|
+
|-------|--------|-------|
|
|
83
|
+
| **Domain** | 80%+ | Business rules, state transitions |
|
|
84
|
+
| **Application** | 70%+ | Use cases, orchestration |
|
|
85
|
+
| **Infrastructure** | 50%+ | Adapters, repositories |
|
|
86
|
+
| **API** | 60%+ | Endpoints, validation |
|
|
87
|
+
| **UI** | 40%+ | Critical user flows |
|
|
88
|
+
|
|
89
|
+
### By Project Size
|
|
90
|
+
|
|
91
|
+
| Size | Unit | Integration | E2E |
|
|
92
|
+
|------|------|-------------|-----|
|
|
93
|
+
| **TOY** | 60%+ | 30%+ | 10%+ |
|
|
94
|
+
| **SMALL** | 70%+ | 40%+ | 20%+ |
|
|
95
|
+
| **MEDIUM** | 75%+ | 50%+ | 25%+ |
|
|
96
|
+
| **LARGE** | 80%+ | 60%+ | 30%+ |
|
|
97
|
+
|
|
98
|
+
**Don't chase 100%** - Focus on critical paths.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## What to Test at Each Level
|
|
103
|
+
|
|
104
|
+
### Unit Tests
|
|
105
|
+
✅ **Test:**
|
|
106
|
+
- Business logic
|
|
107
|
+
- Calculations
|
|
108
|
+
- Validations
|
|
109
|
+
- State transitions
|
|
110
|
+
- Error handling
|
|
111
|
+
|
|
112
|
+
❌ **Don't test:**
|
|
113
|
+
- Private methods
|
|
114
|
+
- Getters/setters
|
|
115
|
+
- Framework code
|
|
116
|
+
|
|
117
|
+
### Integration Tests
|
|
118
|
+
✅ **Test:**
|
|
119
|
+
- Module contracts
|
|
120
|
+
- API boundaries
|
|
121
|
+
- Database operations
|
|
122
|
+
- Event publishing/consuming
|
|
123
|
+
- External service calls
|
|
124
|
+
|
|
125
|
+
❌ **Don't test:**
|
|
126
|
+
- Business logic (unit test it)
|
|
127
|
+
- UI rendering (E2E test it)
|
|
128
|
+
|
|
129
|
+
### E2E Tests
|
|
130
|
+
✅ **Test:**
|
|
131
|
+
- Critical user flows
|
|
132
|
+
- Happy paths
|
|
133
|
+
- Smoke tests
|
|
134
|
+
- Cross-browser (if needed)
|
|
135
|
+
|
|
136
|
+
❌ **Don't test:**
|
|
137
|
+
- Edge cases (unit test them)
|
|
138
|
+
- Error scenarios (integration test them)
|
|
139
|
+
- Every feature (too slow)
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## When to Write Tests
|
|
144
|
+
|
|
145
|
+
### TDD (Test-Driven Development)
|
|
146
|
+
**Process:** Red → Green → Refactor
|
|
147
|
+
|
|
148
|
+
1. **Red:** Write failing test
|
|
149
|
+
2. **Green:** Make it pass (simplest way)
|
|
150
|
+
3. **Refactor:** Improve code
|
|
151
|
+
|
|
152
|
+
**When to use:**
|
|
153
|
+
- Clear requirements
|
|
154
|
+
- Complex business logic
|
|
155
|
+
- API design
|
|
156
|
+
- Bug fixes
|
|
157
|
+
|
|
158
|
+
**Example:**
|
|
159
|
+
```typescript
|
|
160
|
+
// 1. Red - write failing test
|
|
161
|
+
it('calculates discount', () => {
|
|
162
|
+
expect(calculateDiscount(100, 0.1)).toBe(10);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// 2. Green - make it pass
|
|
166
|
+
function calculateDiscount(price, rate) {
|
|
167
|
+
return price * rate;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 3. Refactor - improve
|
|
171
|
+
function calculateDiscount(price: number, rate: number): number {
|
|
172
|
+
if (rate < 0 || rate > 1) throw new Error('Invalid rate');
|
|
173
|
+
return price * rate;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Test After
|
|
178
|
+
Write code first, then tests.
|
|
179
|
+
|
|
180
|
+
**When to use:**
|
|
181
|
+
- Prototyping
|
|
182
|
+
- Unclear requirements
|
|
183
|
+
- Exploratory coding
|
|
184
|
+
- Spikes
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Cost vs Benefit
|
|
189
|
+
|
|
190
|
+
### High Value Tests
|
|
191
|
+
- Critical business logic
|
|
192
|
+
- Payment processing
|
|
193
|
+
- Security features
|
|
194
|
+
- Data integrity
|
|
195
|
+
- User authentication
|
|
196
|
+
|
|
197
|
+
### Medium Value Tests
|
|
198
|
+
- Standard CRUD operations
|
|
199
|
+
- Common workflows
|
|
200
|
+
- API endpoints
|
|
201
|
+
- Form validation
|
|
202
|
+
|
|
203
|
+
### Low Value Tests
|
|
204
|
+
- Simple getters/setters
|
|
205
|
+
- Configuration loading
|
|
206
|
+
- Trivial formatting
|
|
207
|
+
- Pass-through methods
|
|
208
|
+
|
|
209
|
+
**Focus on high-value tests first.**
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Test Maintenance
|
|
214
|
+
|
|
215
|
+
### Keep Tests Green
|
|
216
|
+
- Fix failing tests immediately
|
|
217
|
+
- Don't skip tests
|
|
218
|
+
- Don't comment out tests
|
|
219
|
+
|
|
220
|
+
### Refactor Tests
|
|
221
|
+
- DRY (helper functions)
|
|
222
|
+
- Clear names
|
|
223
|
+
- Remove obsolete tests
|
|
224
|
+
|
|
225
|
+
### Fast Feedback
|
|
226
|
+
- Run unit tests on save
|
|
227
|
+
- Run integration tests on commit
|
|
228
|
+
- Run E2E tests on PR
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## CI/CD Integration
|
|
233
|
+
|
|
234
|
+
```yaml
|
|
235
|
+
# Example CI pipeline
|
|
236
|
+
stages:
|
|
237
|
+
- lint
|
|
238
|
+
- unit-tests # Fast (1-2 min)
|
|
239
|
+
- integration # Medium (5-10 min)
|
|
240
|
+
- e2e # Slow (15-30 min)
|
|
241
|
+
- deploy
|
|
242
|
+
|
|
243
|
+
unit-tests:
|
|
244
|
+
script: npm test
|
|
245
|
+
coverage: 80%
|
|
246
|
+
|
|
247
|
+
integration:
|
|
248
|
+
script: npm run test:integration
|
|
249
|
+
coverage: 50%
|
|
250
|
+
|
|
251
|
+
e2e:
|
|
252
|
+
script: npm run test:e2e
|
|
253
|
+
only: [main, develop]
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Tips
|
|
259
|
+
|
|
260
|
+
**Start with unit tests:**
|
|
261
|
+
- Fastest feedback
|
|
262
|
+
- Easiest to write
|
|
263
|
+
- Most coverage
|
|
264
|
+
|
|
265
|
+
**Add integration tests for boundaries:**
|
|
266
|
+
- Module contracts
|
|
267
|
+
- API endpoints
|
|
268
|
+
- Database operations
|
|
269
|
+
|
|
270
|
+
**E2E tests for critical flows:**
|
|
271
|
+
- User registration
|
|
272
|
+
- Checkout
|
|
273
|
+
- Payment
|
|
274
|
+
- Core features
|
|
275
|
+
|
|
276
|
+
**Measure what matters:**
|
|
277
|
+
- Coverage is a guide, not a goal
|
|
278
|
+
- Focus on critical paths
|
|
279
|
+
- Test behavior, not implementation
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Mocking and Test Isolation
|
|
2
|
+
|
|
3
|
+
How to mock dependencies and isolate tests.
|
|
4
|
+
|
|
5
|
+
## When to Mock
|
|
6
|
+
|
|
7
|
+
**✅ Mock these:**
|
|
8
|
+
- External services (HTTP, DB)
|
|
9
|
+
- Non-deterministic (time, random)
|
|
10
|
+
- Slow operations (file I/O, network)
|
|
11
|
+
- Dependencies you don't control
|
|
12
|
+
|
|
13
|
+
**❌ Don't mock these:**
|
|
14
|
+
- Simple value objects
|
|
15
|
+
- Pure functions
|
|
16
|
+
- Your own domain logic
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Dependency Injection
|
|
21
|
+
|
|
22
|
+
Make code testable by injecting dependencies.
|
|
23
|
+
|
|
24
|
+
**Bad (hardcoded):**
|
|
25
|
+
```typescript
|
|
26
|
+
class OrderService {
|
|
27
|
+
async createOrder(data: OrderData) {
|
|
28
|
+
const db = new Database(); // hardcoded!
|
|
29
|
+
return db.save(data);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Good (injected):**
|
|
35
|
+
```typescript
|
|
36
|
+
class OrderService {
|
|
37
|
+
constructor(private db: Database) {}
|
|
38
|
+
|
|
39
|
+
async createOrder(data: OrderData) {
|
|
40
|
+
return this.db.save(data);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Test
|
|
45
|
+
it('saves order', async () => {
|
|
46
|
+
const mockDb = { save: jest.fn().mockResolvedValue({ id: '123' }) };
|
|
47
|
+
const service = new OrderService(mockDb);
|
|
48
|
+
|
|
49
|
+
await service.createOrder(orderData);
|
|
50
|
+
|
|
51
|
+
expect(mockDb.save).toHaveBeenCalledWith(orderData);
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Interface-Based Mocking (Go)
|
|
58
|
+
|
|
59
|
+
```go
|
|
60
|
+
// Interface
|
|
61
|
+
type Database interface {
|
|
62
|
+
Save(order Order) error
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Real implementation
|
|
66
|
+
type PostgresDB struct {}
|
|
67
|
+
func (db *PostgresDB) Save(order Order) error { /* ... */ }
|
|
68
|
+
|
|
69
|
+
// Mock for tests
|
|
70
|
+
type MockDB struct {
|
|
71
|
+
SaveFunc func(Order) error
|
|
72
|
+
}
|
|
73
|
+
func (m *MockDB) Save(order Order) error {
|
|
74
|
+
return m.SaveFunc(order)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Test
|
|
78
|
+
func TestOrderService_CreateOrder(t *testing.T) {
|
|
79
|
+
mockDB := &MockDB{
|
|
80
|
+
SaveFunc: func(o Order) error {
|
|
81
|
+
return nil // success
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
service := NewOrderService(mockDB)
|
|
86
|
+
err := service.CreateOrder(order)
|
|
87
|
+
|
|
88
|
+
assert.NoError(t, err)
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Mock Frameworks
|
|
95
|
+
|
|
96
|
+
### TypeScript (Jest)
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// Mock function
|
|
100
|
+
const mockSave = jest.fn();
|
|
101
|
+
|
|
102
|
+
// Mock return value
|
|
103
|
+
mockSave.mockResolvedValue({ id: '123' });
|
|
104
|
+
|
|
105
|
+
// Mock error
|
|
106
|
+
mockSave.mockRejectedValue(new Error('DB error'));
|
|
107
|
+
|
|
108
|
+
// Verify calls
|
|
109
|
+
expect(mockSave).toHaveBeenCalledWith(orderData);
|
|
110
|
+
expect(mockSave).toHaveBeenCalledTimes(1);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Python (unittest.mock)
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from unittest.mock import Mock, patch
|
|
117
|
+
|
|
118
|
+
# Mock object
|
|
119
|
+
mock_db = Mock()
|
|
120
|
+
mock_db.save.return_value = {'id': '123'}
|
|
121
|
+
|
|
122
|
+
# Verify calls
|
|
123
|
+
mock_db.save.assert_called_once_with(order_data)
|
|
124
|
+
|
|
125
|
+
# Patch dependency
|
|
126
|
+
@patch('module.Database')
|
|
127
|
+
def test_create_order(mock_db_class):
|
|
128
|
+
mock_db = mock_db_class.return_value
|
|
129
|
+
mock_db.save.return_value = {'id': '123'}
|
|
130
|
+
# test code
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Test Isolation
|
|
136
|
+
|
|
137
|
+
**Rules:**
|
|
138
|
+
|
|
139
|
+
1. **Setup own data** - Don't depend on other tests
|
|
140
|
+
2. **No shared state** - Each test is independent
|
|
141
|
+
3. **Clean up** - Reset state after test
|
|
142
|
+
4. **Run in any order** - No execution dependencies
|
|
143
|
+
|
|
144
|
+
**Example:**
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
describe('OrderRepository', () => {
|
|
148
|
+
let repository: OrderRepository;
|
|
149
|
+
let db: TestDatabase;
|
|
150
|
+
|
|
151
|
+
beforeEach(async () => {
|
|
152
|
+
// Fresh database for each test
|
|
153
|
+
db = await TestDatabase.create();
|
|
154
|
+
repository = new OrderRepository(db);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
afterEach(async () => {
|
|
158
|
+
// Clean up after each test
|
|
159
|
+
await db.destroy();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('saves order', async () => {
|
|
163
|
+
const order = createTestOrder();
|
|
164
|
+
await repository.save(order);
|
|
165
|
+
// Independent of other tests
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Test Doubles
|
|
173
|
+
|
|
174
|
+
### Stub
|
|
175
|
+
Returns predefined values.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const stub = {
|
|
179
|
+
getPrice: () => 100 // always returns 100
|
|
180
|
+
};
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Mock
|
|
184
|
+
Records calls for verification.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const mock = {
|
|
188
|
+
save: jest.fn().mockResolvedValue({ id: '123' })
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Later verify
|
|
192
|
+
expect(mock.save).toHaveBeenCalledWith(data);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Spy
|
|
196
|
+
Wraps real object, records calls.
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
const realService = new OrderService();
|
|
200
|
+
const spy = jest.spyOn(realService, 'createOrder');
|
|
201
|
+
|
|
202
|
+
await realService.createOrder(data);
|
|
203
|
+
|
|
204
|
+
expect(spy).toHaveBeenCalled();
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Fake
|
|
208
|
+
Working implementation (simplified).
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
class FakeDatabase implements Database {
|
|
212
|
+
private data = new Map();
|
|
213
|
+
|
|
214
|
+
async save(order: Order) {
|
|
215
|
+
this.data.set(order.id, order);
|
|
216
|
+
return order;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async find(id: string) {
|
|
220
|
+
return this.data.get(id);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Tips
|
|
228
|
+
|
|
229
|
+
**Prefer fakes over mocks:**
|
|
230
|
+
- Fakes are more realistic
|
|
231
|
+
- Less brittle tests
|
|
232
|
+
- Easier to maintain
|
|
233
|
+
|
|
234
|
+
**Keep mocks simple:**
|
|
235
|
+
- Mock only what you need
|
|
236
|
+
- Don't over-specify
|
|
237
|
+
- Focus on behavior
|
|
238
|
+
|
|
239
|
+
**Avoid mock hell:**
|
|
240
|
+
- Too many mocks = bad design
|
|
241
|
+
- Consider refactoring
|
|
242
|
+
- Use real objects when possible
|
|
243
|
+
|
|
244
|
+
**Test isolation:**
|
|
245
|
+
- Each test is independent
|
|
246
|
+
- No shared state
|
|
247
|
+
- Clean up after yourself
|