@pageai/ralph-loop 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/PROMPT.md +58 -0
- package/.agent/STEERING.md +3 -0
- package/.agent/logs/LOG.md +13 -0
- package/.agent/prd/.gitkeep +0 -0
- package/.agent/screenshots/.gitkeep +0 -0
- package/.agent/skills/component-refactoring/SKILL.md +247 -0
- package/.agent/skills/component-refactoring/references/complexity-patterns.md +485 -0
- package/.agent/skills/component-refactoring/references/component-splitting.md +419 -0
- package/.agent/skills/component-refactoring/references/hook-extraction.md +317 -0
- package/.agent/skills/e2e-tester/SKILL.md +595 -0
- package/.agent/skills/frontend-code-review/SKILL.md +73 -0
- package/.agent/skills/frontend-code-review/references/code-quality.md +28 -0
- package/.agent/skills/frontend-code-review/references/performance.md +36 -0
- package/.agent/skills/frontend-testing/SKILL.md +316 -0
- package/.agent/skills/frontend-testing/assets/component-test.template.tsx +293 -0
- package/.agent/skills/frontend-testing/assets/hook-test.template.ts +207 -0
- package/.agent/skills/frontend-testing/assets/utility-test.template.ts +154 -0
- package/.agent/skills/frontend-testing/references/async-testing.md +345 -0
- package/.agent/skills/frontend-testing/references/checklist.md +188 -0
- package/.agent/skills/frontend-testing/references/common-patterns.md +449 -0
- package/.agent/skills/frontend-testing/references/mocking.md +289 -0
- package/.agent/skills/frontend-testing/references/workflow.md +265 -0
- package/.agent/skills/prd-creator/JSON.md +613 -0
- package/.agent/skills/prd-creator/PRD.md +196 -0
- package/.agent/skills/prd-creator/SKILL.md +143 -0
- package/.agent/skills/skill-creator/SKILL.md +355 -0
- package/.agent/skills/skill-creator/references/output-patterns.md +86 -0
- package/.agent/skills/skill-creator/references/workflows.md +28 -0
- package/.agent/skills/skill-creator/scripts/init_skill.py +300 -0
- package/.agent/skills/skill-creator/scripts/package_skill.py +110 -0
- package/.agent/skills/vercel-react-best-practices/AGENTS.md +2249 -0
- package/.agent/skills/vercel-react-best-practices/SKILL.md +125 -0
- package/.agent/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.agent/skills/vercel-react-best-practices/rules/advanced-use-latest.md +49 -0
- package/.agent/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/.agent/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/.agent/skills/vercel-react-best-practices/rules/async-dependencies.md +36 -0
- package/.agent/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/.agent/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.agent/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.agent/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/.agent/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.agent/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.agent/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/.agent/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/.agent/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +82 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.agent/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.agent/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/.agent/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/.agent/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.agent/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/.agent/skills/vercel-react-best-practices/rules/server-cache-react.md +26 -0
- package/.agent/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +79 -0
- package/.agent/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/.agent/skills/vitest-best-practices/AGENTS.md +84 -0
- package/.agent/skills/vitest-best-practices/SKILL.md +130 -0
- package/.agent/skills/vitest-best-practices/references/aaa-pattern.md +260 -0
- package/.agent/skills/vitest-best-practices/references/assertions.md +393 -0
- package/.agent/skills/vitest-best-practices/references/async-testing.md +454 -0
- package/.agent/skills/vitest-best-practices/references/error-handling.md +382 -0
- package/.agent/skills/vitest-best-practices/references/organization.md +212 -0
- package/.agent/skills/vitest-best-practices/references/parameterized-tests.md +297 -0
- package/.agent/skills/vitest-best-practices/references/performance.md +528 -0
- package/.agent/skills/vitest-best-practices/references/snapshot-testing.md +483 -0
- package/.agent/skills/vitest-best-practices/references/test-doubles.md +499 -0
- package/.agent/skills/vitest-best-practices/references/vitest-features.md +529 -0
- package/.agent/skills/web-design-guidelines/SKILL.md +39 -0
- package/.agent/tasks/.gitkeep +0 -0
- package/.agent/tasks.json +1 -0
- package/.claude/agents/code-reviewer.md +172 -0
- package/.claude/commands/aw.md +50 -0
- package/.claude/hooks/play-sound.js +87 -0
- package/.claude/hooks/pre-tool-use.js +40 -0
- package/.claude/settings.json +54 -0
- package/.claude/settings.local.json +13 -0
- package/.mcp.json +31 -0
- package/AGENTS.md +44 -0
- package/CLAUDE.md +1 -0
- package/README.md +236 -0
- package/bin/cli.js +156 -0
- package/bin/lib/copy.js +149 -0
- package/bin/lib/display.js +137 -0
- package/package.json +65 -0
- package/ralph.sh +333 -0
- package/scripts/lib/args.sh +44 -0
- package/scripts/lib/cleanup.sh +53 -0
- package/scripts/lib/constants.sh +25 -0
- package/scripts/lib/display.sh +196 -0
- package/scripts/lib/logging.sh +30 -0
- package/scripts/lib/notify.sh +41 -0
- package/scripts/lib/output.sh +147 -0
- package/scripts/lib/preflight.sh +57 -0
- package/scripts/lib/preview.sh +77 -0
- package/scripts/lib/promise.sh +76 -0
- package/scripts/lib/spinner.sh +85 -0
- package/scripts/lib/terminal.sh +57 -0
- package/scripts/lib/timing.sh +223 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
# 1.6 Test Doubles
|
|
2
|
+
|
|
3
|
+
Test doubles replace real dependencies in tests. Use them sparingly and prefer real implementations when practical.
|
|
4
|
+
|
|
5
|
+
## Hierarchy of Test Doubles
|
|
6
|
+
|
|
7
|
+
Prefer in this order (best to worst):
|
|
8
|
+
|
|
9
|
+
1. **Real Implementation**: Use the actual code whenever possible
|
|
10
|
+
2. **Fakes**: Lightweight working implementations (e.g., in-memory database)
|
|
11
|
+
3. **Stubs**: Return pre-configured responses without behavior
|
|
12
|
+
4. **Spies**: Record calls while allowing real implementation to run
|
|
13
|
+
5. **Mocks**: Replace behavior AND verify interactions (last resort)
|
|
14
|
+
|
|
15
|
+
## When to Use Test Doubles
|
|
16
|
+
|
|
17
|
+
**✅ DO mock these:**
|
|
18
|
+
- External services (APIs, databases, file systems)
|
|
19
|
+
- Third-party libraries you don't control
|
|
20
|
+
- Non-deterministic functions (Date.now(), Math.random())
|
|
21
|
+
- Slow operations (network calls, large file I/O)
|
|
22
|
+
|
|
23
|
+
**❌ DON'T mock these:**
|
|
24
|
+
- Pure functions (deterministic, no side effects)
|
|
25
|
+
- Your own application code
|
|
26
|
+
- Simple utilities (array helpers, formatters)
|
|
27
|
+
- Code you're actively testing
|
|
28
|
+
|
|
29
|
+
## 1. Real Implementation (Preferred)
|
|
30
|
+
|
|
31
|
+
Use real code whenever possible - it provides the most confidence.
|
|
32
|
+
|
|
33
|
+
**✅ Correct: using real implementation**
|
|
34
|
+
```ts
|
|
35
|
+
describe('OrderService', () => {
|
|
36
|
+
it('should calculate order total correctly', () => {
|
|
37
|
+
const priceCalculator = new PriceCalculator(); // Real implementation
|
|
38
|
+
const orderService = new OrderService(priceCalculator);
|
|
39
|
+
|
|
40
|
+
const order = orderService.createOrder([
|
|
41
|
+
{ id: 1, price: 10, quantity: 2 },
|
|
42
|
+
{ id: 2, price: 5, quantity: 3 },
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
expect(order.total).toEqual(35);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 2. Fakes
|
|
51
|
+
|
|
52
|
+
Fakes are lightweight implementations with real behavior.
|
|
53
|
+
|
|
54
|
+
**✅ Correct: in-memory fake for database**
|
|
55
|
+
```ts
|
|
56
|
+
class FakeUserRepository implements UserRepository {
|
|
57
|
+
private users: Map<string, User> = new Map();
|
|
58
|
+
|
|
59
|
+
async save(user: User): Promise<void> {
|
|
60
|
+
this.users.set(user.id, user);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async findById(id: string): Promise<User | null> {
|
|
64
|
+
return this.users.get(id) || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async findAll(): Promise<User[]> {
|
|
68
|
+
return Array.from(this.users.values());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
clear() {
|
|
72
|
+
this.users.clear();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('UserService', () => {
|
|
77
|
+
let userRepo: FakeUserRepository;
|
|
78
|
+
let userService: UserService;
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
userRepo = new FakeUserRepository();
|
|
82
|
+
userService = new UserService(userRepo);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should create and retrieve user', async () => {
|
|
86
|
+
const user = await userService.createUser({
|
|
87
|
+
name: 'John',
|
|
88
|
+
email: 'john@example.com',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const retrieved = await userService.getUser(user.id);
|
|
92
|
+
|
|
93
|
+
expect(retrieved).toEqual(user);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should list all users', async () => {
|
|
97
|
+
await userService.createUser({ name: 'Alice', email: 'alice@example.com' });
|
|
98
|
+
await userService.createUser({ name: 'Bob', email: 'bob@example.com' });
|
|
99
|
+
|
|
100
|
+
const users = await userService.getAllUsers();
|
|
101
|
+
|
|
102
|
+
expect(users).toHaveLength(2);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**✅ Correct: fake for external API**
|
|
108
|
+
```ts
|
|
109
|
+
class FakePaymentGateway implements PaymentGateway {
|
|
110
|
+
private payments: Payment[] = [];
|
|
111
|
+
|
|
112
|
+
async charge(amount: number, token: string): Promise<PaymentResult> {
|
|
113
|
+
const payment: Payment = {
|
|
114
|
+
id: `pay_${Date.now()}`,
|
|
115
|
+
amount,
|
|
116
|
+
token,
|
|
117
|
+
status: 'succeeded',
|
|
118
|
+
createdAt: new Date(),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
this.payments.push(payment);
|
|
122
|
+
|
|
123
|
+
return { success: true, paymentId: payment.id };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getPayments(): Payment[] {
|
|
127
|
+
return [...this.payments];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 3. Stubs
|
|
133
|
+
|
|
134
|
+
Stubs return pre-configured responses without implementing real behavior.
|
|
135
|
+
|
|
136
|
+
**✅ Correct: stubbing external API call**
|
|
137
|
+
```ts
|
|
138
|
+
describe('WeatherService', () => {
|
|
139
|
+
it('should return temperature for city', async () => {
|
|
140
|
+
const apiClient = {
|
|
141
|
+
fetch: vi.fn().mockResolvedValue({
|
|
142
|
+
temperature: 72,
|
|
143
|
+
conditions: 'sunny',
|
|
144
|
+
city: 'San Francisco',
|
|
145
|
+
}),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const weatherService = new WeatherService(apiClient);
|
|
149
|
+
const weather = await weatherService.getWeather('San Francisco');
|
|
150
|
+
|
|
151
|
+
expect(weather.temperature).toEqual(72);
|
|
152
|
+
expect(apiClient.fetch).toHaveBeenCalledWith('/weather?city=San+Francisco');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should handle API errors', async () => {
|
|
156
|
+
const apiClient = {
|
|
157
|
+
fetch: vi.fn().mockRejectedValue(new Error('API unavailable')),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const weatherService = new WeatherService(apiClient);
|
|
161
|
+
|
|
162
|
+
await expect(weatherService.getWeather('Invalid'))
|
|
163
|
+
.rejects.toThrow('API unavailable');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**✅ Correct: stubbing multiple scenarios**
|
|
169
|
+
```ts
|
|
170
|
+
describe('DataService', () => {
|
|
171
|
+
it('should retry on failure then succeed', async () => {
|
|
172
|
+
const apiClient = {
|
|
173
|
+
fetch: vi.fn()
|
|
174
|
+
.mockRejectedValueOnce(new Error('Timeout'))
|
|
175
|
+
.mockRejectedValueOnce(new Error('Timeout'))
|
|
176
|
+
.mockResolvedValueOnce({ data: 'success' }),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const service = new DataService(apiClient);
|
|
180
|
+
const result = await service.fetchWithRetry('/api/data');
|
|
181
|
+
|
|
182
|
+
expect(result).toEqual({ data: 'success' });
|
|
183
|
+
expect(apiClient.fetch).toHaveBeenCalledTimes(3);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## 4. Spies
|
|
189
|
+
|
|
190
|
+
Spies record function calls while preserving original behavior.
|
|
191
|
+
|
|
192
|
+
**✅ Correct: spying on method calls**
|
|
193
|
+
```ts
|
|
194
|
+
describe('Logger', () => {
|
|
195
|
+
it('should log errors to console', () => {
|
|
196
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
197
|
+
|
|
198
|
+
const logger = new Logger();
|
|
199
|
+
logger.error('Something went wrong');
|
|
200
|
+
|
|
201
|
+
expect(consoleSpy).toHaveBeenCalledWith('[ERROR]', 'Something went wrong');
|
|
202
|
+
|
|
203
|
+
consoleSpy.mockRestore();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**✅ Correct: spying to verify side effects**
|
|
209
|
+
```ts
|
|
210
|
+
describe('Analytics', () => {
|
|
211
|
+
it('should track page views', () => {
|
|
212
|
+
const trackingSpy = vi.fn();
|
|
213
|
+
const analytics = new Analytics({ track: trackingSpy });
|
|
214
|
+
|
|
215
|
+
analytics.pageView('/home', { userId: '123' });
|
|
216
|
+
|
|
217
|
+
expect(trackingSpy).toHaveBeenCalledWith('pageview', {
|
|
218
|
+
path: '/home',
|
|
219
|
+
userId: '123',
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## 5. Mocks (Use Sparingly)
|
|
226
|
+
|
|
227
|
+
Mocks replace behavior AND verify interactions. Use only when necessary.
|
|
228
|
+
|
|
229
|
+
**✅ Correct: mocking external service**
|
|
230
|
+
```ts
|
|
231
|
+
describe('EmailService', () => {
|
|
232
|
+
it('should send welcome email to new users', async () => {
|
|
233
|
+
const emailProvider = {
|
|
234
|
+
send: vi.fn().mockResolvedValue({ messageId: 'msg-123' }),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const emailService = new EmailService(emailProvider);
|
|
238
|
+
await emailService.sendWelcomeEmail('user@example.com');
|
|
239
|
+
|
|
240
|
+
expect(emailProvider.send).toHaveBeenCalledWith({
|
|
241
|
+
to: 'user@example.com',
|
|
242
|
+
subject: 'Welcome!',
|
|
243
|
+
body: expect.stringContaining('Welcome'),
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**❌ Incorrect: over-mocking internal code**
|
|
250
|
+
```ts
|
|
251
|
+
describe('OrderProcessor', () => {
|
|
252
|
+
it('should process order', () => {
|
|
253
|
+
const calculateTaxMock = vi.fn().mockReturnValue(5);
|
|
254
|
+
const calculateShippingMock = vi.fn().mockReturnValue(10);
|
|
255
|
+
const formatPriceMock = vi.fn().mockReturnValue('$50.00');
|
|
256
|
+
|
|
257
|
+
// Too many mocks! Just use real implementations
|
|
258
|
+
const processor = new OrderProcessor({
|
|
259
|
+
calculateTax: calculateTaxMock,
|
|
260
|
+
calculateShipping: calculateShippingMock,
|
|
261
|
+
formatPrice: formatPriceMock,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ...test logic
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
*Why?* Mocking your own simple functions makes tests brittle and less valuable. Use real implementations.
|
|
269
|
+
|
|
270
|
+
## vi.fn() - Manual Mocks
|
|
271
|
+
|
|
272
|
+
Create mock functions when you need fine control.
|
|
273
|
+
|
|
274
|
+
**✅ Correct: creating mock with specific behavior**
|
|
275
|
+
```ts
|
|
276
|
+
describe('DataFetcher', () => {
|
|
277
|
+
it('should handle pagination', async () => {
|
|
278
|
+
const fetchFn = vi.fn()
|
|
279
|
+
.mockResolvedValueOnce({ items: [1, 2, 3], hasMore: true })
|
|
280
|
+
.mockResolvedValueOnce({ items: [4, 5, 6], hasMore: true })
|
|
281
|
+
.mockResolvedValueOnce({ items: [7, 8], hasMore: false });
|
|
282
|
+
|
|
283
|
+
const fetcher = new DataFetcher(fetchFn);
|
|
284
|
+
const allItems = await fetcher.fetchAll();
|
|
285
|
+
|
|
286
|
+
expect(allItems).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
|
|
287
|
+
expect(fetchFn).toHaveBeenCalledTimes(3);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## vi.mock() - Module Mocking
|
|
293
|
+
|
|
294
|
+
Mock entire modules when you need to replace external dependencies.
|
|
295
|
+
|
|
296
|
+
**✅ Correct: mocking external module**
|
|
297
|
+
```ts
|
|
298
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
299
|
+
|
|
300
|
+
vi.mock('uuid', () => ({
|
|
301
|
+
v4: vi.fn(),
|
|
302
|
+
}));
|
|
303
|
+
|
|
304
|
+
describe('UserService', () => {
|
|
305
|
+
it('should generate unique user IDs', () => {
|
|
306
|
+
vi.mocked(uuidv4)
|
|
307
|
+
.mockReturnValueOnce('id-1')
|
|
308
|
+
.mockReturnValueOnce('id-2');
|
|
309
|
+
|
|
310
|
+
const service = new UserService();
|
|
311
|
+
const user1 = service.createUser({ name: 'Alice' });
|
|
312
|
+
const user2 = service.createUser({ name: 'Bob' });
|
|
313
|
+
|
|
314
|
+
expect(user1.id).toBe('id-1');
|
|
315
|
+
expect(user2.id).toBe('id-2');
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**✅ Correct: partial module mock**
|
|
321
|
+
```ts
|
|
322
|
+
vi.mock('../utils', async () => {
|
|
323
|
+
const actual = await vi.importActual('../utils');
|
|
324
|
+
return {
|
|
325
|
+
...actual,
|
|
326
|
+
fetchData: vi.fn(), // Only mock fetchData, keep other utils real
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Mocking Timers
|
|
332
|
+
|
|
333
|
+
Use fake timers for testing time-dependent code.
|
|
334
|
+
|
|
335
|
+
**✅ Correct: testing debounce with fake timers**
|
|
336
|
+
```ts
|
|
337
|
+
describe('debounce', () => {
|
|
338
|
+
beforeEach(() => {
|
|
339
|
+
vi.useFakeTimers();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
afterEach(() => {
|
|
343
|
+
vi.useRealTimers();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should delay function execution', () => {
|
|
347
|
+
const callback = vi.fn();
|
|
348
|
+
const debounced = debounce(callback, 1000);
|
|
349
|
+
|
|
350
|
+
debounced();
|
|
351
|
+
debounced();
|
|
352
|
+
debounced();
|
|
353
|
+
|
|
354
|
+
expect(callback).not.toHaveBeenCalled();
|
|
355
|
+
|
|
356
|
+
vi.advanceTimersByTime(1000);
|
|
357
|
+
|
|
358
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**✅ Correct: testing intervals**
|
|
364
|
+
```ts
|
|
365
|
+
it('should poll every 5 seconds', () => {
|
|
366
|
+
vi.useFakeTimers();
|
|
367
|
+
|
|
368
|
+
const pollFn = vi.fn();
|
|
369
|
+
const poller = new Poller(pollFn, 5000);
|
|
370
|
+
|
|
371
|
+
poller.start();
|
|
372
|
+
|
|
373
|
+
expect(pollFn).toHaveBeenCalledTimes(1);
|
|
374
|
+
|
|
375
|
+
vi.advanceTimersByTime(5000);
|
|
376
|
+
expect(pollFn).toHaveBeenCalledTimes(2);
|
|
377
|
+
|
|
378
|
+
vi.advanceTimersByTime(5000);
|
|
379
|
+
expect(pollFn).toHaveBeenCalledTimes(3);
|
|
380
|
+
|
|
381
|
+
poller.stop();
|
|
382
|
+
vi.useRealTimers();
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Testing with Dates
|
|
387
|
+
|
|
388
|
+
Mock dates for consistent test results.
|
|
389
|
+
|
|
390
|
+
**✅ Correct: mocking current date**
|
|
391
|
+
```ts
|
|
392
|
+
describe('isExpired', () => {
|
|
393
|
+
beforeEach(() => {
|
|
394
|
+
vi.useFakeTimers();
|
|
395
|
+
vi.setSystemTime(new Date('2024-01-01'));
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
afterEach(() => {
|
|
399
|
+
vi.useRealTimers();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should return true for expired items', () => {
|
|
403
|
+
const expiredItem = { expiryDate: new Date('2023-12-31') };
|
|
404
|
+
expect(isExpired(expiredItem)).toBe(true);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('should return false for valid items', () => {
|
|
408
|
+
const validItem = { expiryDate: new Date('2024-12-31') };
|
|
409
|
+
expect(isExpired(validItem)).toBe(false);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Clearing and Restoring Mocks
|
|
415
|
+
|
|
416
|
+
Always clean up mocks between tests.
|
|
417
|
+
|
|
418
|
+
**✅ Correct: clearing mocks**
|
|
419
|
+
```ts
|
|
420
|
+
describe('UserService', () => {
|
|
421
|
+
const mockApi = {
|
|
422
|
+
fetchUser: vi.fn(),
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
beforeEach(() => {
|
|
426
|
+
mockApi.fetchUser.mockClear(); // Clear call history
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('test 1', async () => {
|
|
430
|
+
mockApi.fetchUser.mockResolvedValue({ id: '1', name: 'Alice' });
|
|
431
|
+
// ... test logic
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('test 2', async () => {
|
|
435
|
+
mockApi.fetchUser.mockResolvedValue({ id: '2', name: 'Bob' });
|
|
436
|
+
// ... test logic
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**✅ Correct: restoring spies**
|
|
442
|
+
```ts
|
|
443
|
+
describe('Logger', () => {
|
|
444
|
+
afterEach(() => {
|
|
445
|
+
vi.restoreAllMocks(); // Restore all spies
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should log to console', () => {
|
|
449
|
+
const spy = vi.spyOn(console, 'log');
|
|
450
|
+
// ... test logic
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Anti-Patterns
|
|
456
|
+
|
|
457
|
+
**❌ Incorrect: mocking everything**
|
|
458
|
+
```ts
|
|
459
|
+
it('should create user', () => {
|
|
460
|
+
const mockValidate = vi.fn().mockReturnValue(true);
|
|
461
|
+
const mockHash = vi.fn().mockReturnValue('hashed');
|
|
462
|
+
const mockGenId = vi.fn().mockReturnValue('id-123');
|
|
463
|
+
const mockSave = vi.fn();
|
|
464
|
+
|
|
465
|
+
// Way too many mocks - just use real code!
|
|
466
|
+
const service = new UserService({
|
|
467
|
+
validate: mockValidate,
|
|
468
|
+
hash: mockHash,
|
|
469
|
+
generateId: mockGenId,
|
|
470
|
+
save: mockSave,
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
**❌ Incorrect: testing mocks instead of behavior**
|
|
476
|
+
```ts
|
|
477
|
+
it('should call formatUser', () => {
|
|
478
|
+
const mockFormat = vi.fn();
|
|
479
|
+
service.formatUser = mockFormat;
|
|
480
|
+
|
|
481
|
+
service.getUser('123');
|
|
482
|
+
|
|
483
|
+
expect(mockFormat).toHaveBeenCalled(); // Who cares if it was called?
|
|
484
|
+
});
|
|
485
|
+
```
|
|
486
|
+
*Why?* Test the actual behavior (what the user gets), not internal implementation (which functions were called).
|
|
487
|
+
|
|
488
|
+
**❌ Incorrect: brittle interaction testing**
|
|
489
|
+
```ts
|
|
490
|
+
it('should process user', () => {
|
|
491
|
+
service.processUser(user);
|
|
492
|
+
|
|
493
|
+
expect(logger.log).toHaveBeenCalledTimes(3); // Fragile!
|
|
494
|
+
expect(logger.log).toHaveBeenNthCalledWith(1, 'start');
|
|
495
|
+
expect(logger.log).toHaveBeenNthCalledWith(2, 'processing');
|
|
496
|
+
expect(logger.log).toHaveBeenNthCalledWith(3, 'done');
|
|
497
|
+
});
|
|
498
|
+
```
|
|
499
|
+
*Why?* This test breaks if you change logging details, even though the actual functionality works fine.
|