developer-ai 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/README.md +241 -0
- package/bin/developer-ai.js +2 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +219 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +82 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/schema.d.ts +115 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +29 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +8 -0
- package/dist/constants.js.map +1 -0
- package/dist/core/agent.d.ts +38 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +155 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/system-prompt.d.ts +6 -0
- package/dist/core/system-prompt.d.ts.map +1 -0
- package/dist/core/system-prompt.js +44 -0
- package/dist/core/system-prompt.js.map +1 -0
- package/dist/core/types.d.ts +42 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +6 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/client.d.ts +13 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +202 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/providers/ollama.d.ts +13 -0
- package/dist/providers/ollama.d.ts.map +1 -0
- package/dist/providers/ollama.js +60 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/openai.d.ts +9 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +40 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/skills/loader.d.ts +25 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +93 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/tests/tools.test.d.ts +2 -0
- package/dist/tests/tools.test.d.ts.map +1 -0
- package/dist/tests/tools.test.js +170 -0
- package/dist/tests/tools.test.js.map +1 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +19 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/list-files.d.ts +3 -0
- package/dist/tools/list-files.d.ts.map +1 -0
- package/dist/tools/list-files.js +60 -0
- package/dist/tools/list-files.js.map +1 -0
- package/dist/tools/read-file.d.ts +3 -0
- package/dist/tools/read-file.d.ts.map +1 -0
- package/dist/tools/read-file.js +46 -0
- package/dist/tools/read-file.js.map +1 -0
- package/dist/tools/registry.d.ts +24 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +37 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/run-command.d.ts +3 -0
- package/dist/tools/run-command.d.ts.map +1 -0
- package/dist/tools/run-command.js +114 -0
- package/dist/tools/run-command.js.map +1 -0
- package/dist/tools/search-text.d.ts +3 -0
- package/dist/tools/search-text.d.ts.map +1 -0
- package/dist/tools/search-text.js +103 -0
- package/dist/tools/search-text.js.map +1 -0
- package/dist/tools/utils.d.ts +6 -0
- package/dist/tools/utils.d.ts.map +1 -0
- package/dist/tools/utils.js +14 -0
- package/dist/tools/utils.js.map +1 -0
- package/dist/tools/web-search.d.ts +3 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +80 -0
- package/dist/tools/web-search.js.map +1 -0
- package/dist/tools/write-file.d.ts +3 -0
- package/dist/tools/write-file.d.ts.map +1 -0
- package/dist/tools/write-file.js +66 -0
- package/dist/tools/write-file.js.map +1 -0
- package/package.json +54 -0
- package/skills/accessibility/SKILL.md +496 -0
- package/skills/api-design/SKILL.md +419 -0
- package/skills/code-review/SKILL.md +267 -0
- package/skills/debugging/SKILL.md +332 -0
- package/skills/documentation/SKILL.md +496 -0
- package/skills/error-handling/SKILL.md +504 -0
- package/skills/git-workflow/SKILL.md +448 -0
- package/skills/human-like-coding/SKILL.md +400 -0
- package/skills/performance-optimization/SKILL.md +412 -0
- package/skills/prompt-engineering/SKILL.md +362 -0
- package/skills/refactoring/SKILL.md +457 -0
- package/skills/security-audit/SKILL.md +453 -0
- package/skills/testing-strategy/SKILL.md +501 -0
- package/skills/webapp-testing/SKILL.md +309 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing-strategy
|
|
3
|
+
description: Guide for developing comprehensive testing strategies. Use when planning test coverage, choosing testing frameworks, or establishing testing practices. Covers testing pyramid, unit/integration/E2E tests, TDD, and test organization.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Testing Strategy Skill
|
|
7
|
+
|
|
8
|
+
This skill provides guidance for developing effective testing strategies and writing maintainable tests.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
A good testing strategy balances confidence, speed, and maintainability. This skill covers the testing pyramid, different test types, and best practices.
|
|
13
|
+
|
|
14
|
+
## Testing Pyramid
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
/\
|
|
18
|
+
/ \ E2E Tests
|
|
19
|
+
/----\ (Few, slow, high confidence)
|
|
20
|
+
/ \
|
|
21
|
+
/--------\ Integration Tests
|
|
22
|
+
/ \ (Some, medium speed)
|
|
23
|
+
/------------\
|
|
24
|
+
/ \ Unit Tests
|
|
25
|
+
/________________\ (Many, fast, focused)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Test Distribution
|
|
29
|
+
|
|
30
|
+
| Type | Coverage | Speed | Quantity |
|
|
31
|
+
|------|----------|-------|----------|
|
|
32
|
+
| Unit | Single function/component | Fast (ms) | ~70% |
|
|
33
|
+
| Integration | Multiple modules | Medium (s) | ~20% |
|
|
34
|
+
| E2E | Full user flows | Slow (s-min) | ~10% |
|
|
35
|
+
|
|
36
|
+
## Unit Testing
|
|
37
|
+
|
|
38
|
+
### Structure (AAA Pattern)
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
describe('calculateDiscount', () => {
|
|
42
|
+
it('applies 10% discount for orders over $100', () => {
|
|
43
|
+
// Arrange
|
|
44
|
+
const order = { subtotal: 150 };
|
|
45
|
+
|
|
46
|
+
// Act
|
|
47
|
+
const discount = calculateDiscount(order);
|
|
48
|
+
|
|
49
|
+
// Assert
|
|
50
|
+
expect(discount).toBe(15);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Naming Conventions
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
// Pattern: "should [expected behavior] when [condition]"
|
|
59
|
+
describe('UserService', () => {
|
|
60
|
+
describe('createUser', () => {
|
|
61
|
+
it('should create user when valid data provided', () => {});
|
|
62
|
+
it('should throw ValidationError when email is invalid', () => {});
|
|
63
|
+
it('should throw ConflictError when email already exists', () => {});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Testing Pure Functions
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
// Pure functions are easiest to test
|
|
72
|
+
function sum(numbers) {
|
|
73
|
+
return numbers.reduce((a, b) => a + b, 0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('sum', () => {
|
|
77
|
+
it('returns 0 for empty array', () => {
|
|
78
|
+
expect(sum([])).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns the number for single-element array', () => {
|
|
82
|
+
expect(sum([5])).toBe(5);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('sums all numbers in array', () => {
|
|
86
|
+
expect(sum([1, 2, 3, 4])).toBe(10);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('handles negative numbers', () => {
|
|
90
|
+
expect(sum([1, -2, 3])).toBe(2);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Testing with Mocks
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
import { vi } from 'vitest';
|
|
99
|
+
|
|
100
|
+
// Mock external dependencies
|
|
101
|
+
const mockDatabase = {
|
|
102
|
+
findUser: vi.fn(),
|
|
103
|
+
saveUser: vi.fn(),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
describe('UserService', () => {
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
vi.clearAllMocks();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('creates user in database', async () => {
|
|
112
|
+
mockDatabase.saveUser.mockResolvedValue({ id: '123', name: 'John' });
|
|
113
|
+
|
|
114
|
+
const service = new UserService(mockDatabase);
|
|
115
|
+
const user = await service.createUser({ name: 'John' });
|
|
116
|
+
|
|
117
|
+
expect(mockDatabase.saveUser).toHaveBeenCalledWith({ name: 'John' });
|
|
118
|
+
expect(user.id).toBe('123');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('throws when database fails', async () => {
|
|
122
|
+
mockDatabase.saveUser.mockRejectedValue(new Error('DB Error'));
|
|
123
|
+
|
|
124
|
+
const service = new UserService(mockDatabase);
|
|
125
|
+
|
|
126
|
+
await expect(service.createUser({ name: 'John' }))
|
|
127
|
+
.rejects.toThrow('DB Error');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Integration Testing
|
|
133
|
+
|
|
134
|
+
### API Integration Tests
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
138
|
+
import request from 'supertest';
|
|
139
|
+
import app from './app';
|
|
140
|
+
|
|
141
|
+
describe('POST /api/users', () => {
|
|
142
|
+
beforeAll(async () => {
|
|
143
|
+
await setupTestDatabase();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
afterAll(async () => {
|
|
147
|
+
await cleanupTestDatabase();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('creates a new user', async () => {
|
|
151
|
+
const response = await request(app)
|
|
152
|
+
.post('/api/users')
|
|
153
|
+
.send({ name: 'John', email: 'john@example.com' })
|
|
154
|
+
.expect(201);
|
|
155
|
+
|
|
156
|
+
expect(response.body).toMatchObject({
|
|
157
|
+
name: 'John',
|
|
158
|
+
email: 'john@example.com',
|
|
159
|
+
});
|
|
160
|
+
expect(response.body.id).toBeDefined();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('returns 400 for invalid email', async () => {
|
|
164
|
+
const response = await request(app)
|
|
165
|
+
.post('/api/users')
|
|
166
|
+
.send({ name: 'John', email: 'invalid' })
|
|
167
|
+
.expect(400);
|
|
168
|
+
|
|
169
|
+
expect(response.body.error.code).toBe('VALIDATION_ERROR');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('returns 409 for duplicate email', async () => {
|
|
173
|
+
// First request succeeds
|
|
174
|
+
await request(app)
|
|
175
|
+
.post('/api/users')
|
|
176
|
+
.send({ name: 'John', email: 'duplicate@example.com' })
|
|
177
|
+
.expect(201);
|
|
178
|
+
|
|
179
|
+
// Second request with same email fails
|
|
180
|
+
const response = await request(app)
|
|
181
|
+
.post('/api/users')
|
|
182
|
+
.send({ name: 'Jane', email: 'duplicate@example.com' })
|
|
183
|
+
.expect(409);
|
|
184
|
+
|
|
185
|
+
expect(response.body.error.code).toBe('CONFLICT');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Database Integration Tests
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
import { PrismaClient } from '@prisma/client';
|
|
194
|
+
|
|
195
|
+
const prisma = new PrismaClient();
|
|
196
|
+
|
|
197
|
+
describe('UserRepository', () => {
|
|
198
|
+
beforeEach(async () => {
|
|
199
|
+
// Clean database before each test
|
|
200
|
+
await prisma.user.deleteMany();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
afterAll(async () => {
|
|
204
|
+
await prisma.$disconnect();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('creates and retrieves user', async () => {
|
|
208
|
+
const created = await prisma.user.create({
|
|
209
|
+
data: { name: 'John', email: 'john@example.com' },
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const found = await prisma.user.findUnique({
|
|
213
|
+
where: { id: created.id },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(found).toEqual(created);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## React Component Testing
|
|
222
|
+
|
|
223
|
+
### Component Unit Tests
|
|
224
|
+
|
|
225
|
+
```javascript
|
|
226
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
227
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
228
|
+
import Button from './Button';
|
|
229
|
+
|
|
230
|
+
describe('Button', () => {
|
|
231
|
+
it('renders children', () => {
|
|
232
|
+
render(<Button>Click me</Button>);
|
|
233
|
+
|
|
234
|
+
expect(screen.getByRole('button')).toHaveTextContent('Click me');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('calls onClick when clicked', () => {
|
|
238
|
+
const handleClick = vi.fn();
|
|
239
|
+
render(<Button onClick={handleClick}>Click me</Button>);
|
|
240
|
+
|
|
241
|
+
fireEvent.click(screen.getByRole('button'));
|
|
242
|
+
|
|
243
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('is disabled when disabled prop is true', () => {
|
|
247
|
+
render(<Button disabled>Click me</Button>);
|
|
248
|
+
|
|
249
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Testing with User Events
|
|
255
|
+
|
|
256
|
+
```javascript
|
|
257
|
+
import { render, screen } from '@testing-library/react';
|
|
258
|
+
import userEvent from '@testing-library/user-event';
|
|
259
|
+
import LoginForm from './LoginForm';
|
|
260
|
+
|
|
261
|
+
describe('LoginForm', () => {
|
|
262
|
+
it('submits form with entered values', async () => {
|
|
263
|
+
const user = userEvent.setup();
|
|
264
|
+
const handleSubmit = vi.fn();
|
|
265
|
+
|
|
266
|
+
render(<LoginForm onSubmit={handleSubmit} />);
|
|
267
|
+
|
|
268
|
+
await user.type(screen.getByLabelText(/email/i), 'john@example.com');
|
|
269
|
+
await user.type(screen.getByLabelText(/password/i), 'password123');
|
|
270
|
+
await user.click(screen.getByRole('button', { name: /submit/i }));
|
|
271
|
+
|
|
272
|
+
expect(handleSubmit).toHaveBeenCalledWith({
|
|
273
|
+
email: 'john@example.com',
|
|
274
|
+
password: 'password123',
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('shows error for invalid email', async () => {
|
|
279
|
+
const user = userEvent.setup();
|
|
280
|
+
render(<LoginForm onSubmit={() => {}} />);
|
|
281
|
+
|
|
282
|
+
await user.type(screen.getByLabelText(/email/i), 'invalid');
|
|
283
|
+
await user.click(screen.getByRole('button', { name: /submit/i }));
|
|
284
|
+
|
|
285
|
+
expect(screen.getByText(/invalid email/i)).toBeInTheDocument();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Testing Async Components
|
|
291
|
+
|
|
292
|
+
```javascript
|
|
293
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
294
|
+
import UserProfile from './UserProfile';
|
|
295
|
+
import { mockFetch } from './testUtils';
|
|
296
|
+
|
|
297
|
+
describe('UserProfile', () => {
|
|
298
|
+
it('displays user data after loading', async () => {
|
|
299
|
+
mockFetch({ name: 'John Doe', email: 'john@example.com' });
|
|
300
|
+
|
|
301
|
+
render(<UserProfile userId="123" />);
|
|
302
|
+
|
|
303
|
+
// Initially shows loading
|
|
304
|
+
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
|
305
|
+
|
|
306
|
+
// Wait for data to appear
|
|
307
|
+
await waitFor(() => {
|
|
308
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('displays error message on failure', async () => {
|
|
315
|
+
mockFetch(null, { status: 500 });
|
|
316
|
+
|
|
317
|
+
render(<UserProfile userId="123" />);
|
|
318
|
+
|
|
319
|
+
await waitFor(() => {
|
|
320
|
+
expect(screen.getByText(/error/i)).toBeInTheDocument();
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## E2E Testing with Playwright
|
|
327
|
+
|
|
328
|
+
### Basic E2E Test
|
|
329
|
+
|
|
330
|
+
```javascript
|
|
331
|
+
import { test, expect } from '@playwright/test';
|
|
332
|
+
|
|
333
|
+
test.describe('User Registration', () => {
|
|
334
|
+
test('registers a new user', async ({ page }) => {
|
|
335
|
+
await page.goto('/register');
|
|
336
|
+
|
|
337
|
+
await page.fill('[name="name"]', 'John Doe');
|
|
338
|
+
await page.fill('[name="email"]', 'john@example.com');
|
|
339
|
+
await page.fill('[name="password"]', 'SecurePass123!');
|
|
340
|
+
|
|
341
|
+
await page.click('button[type="submit"]');
|
|
342
|
+
|
|
343
|
+
// Should redirect to dashboard
|
|
344
|
+
await expect(page).toHaveURL('/dashboard');
|
|
345
|
+
await expect(page.locator('h1')).toContainText('Welcome, John');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('shows validation errors', async ({ page }) => {
|
|
349
|
+
await page.goto('/register');
|
|
350
|
+
|
|
351
|
+
await page.fill('[name="email"]', 'invalid');
|
|
352
|
+
await page.click('button[type="submit"]');
|
|
353
|
+
|
|
354
|
+
await expect(page.locator('.error')).toContainText('Invalid email');
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Page Object Pattern
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
// pages/LoginPage.js
|
|
363
|
+
export class LoginPage {
|
|
364
|
+
constructor(page) {
|
|
365
|
+
this.page = page;
|
|
366
|
+
this.emailInput = page.locator('[name="email"]');
|
|
367
|
+
this.passwordInput = page.locator('[name="password"]');
|
|
368
|
+
this.submitButton = page.locator('button[type="submit"]');
|
|
369
|
+
this.errorMessage = page.locator('.error-message');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async goto() {
|
|
373
|
+
await this.page.goto('/login');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async login(email, password) {
|
|
377
|
+
await this.emailInput.fill(email);
|
|
378
|
+
await this.passwordInput.fill(password);
|
|
379
|
+
await this.submitButton.click();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// tests/login.spec.js
|
|
384
|
+
import { test, expect } from '@playwright/test';
|
|
385
|
+
import { LoginPage } from '../pages/LoginPage';
|
|
386
|
+
|
|
387
|
+
test('successful login', async ({ page }) => {
|
|
388
|
+
const loginPage = new LoginPage(page);
|
|
389
|
+
await loginPage.goto();
|
|
390
|
+
await loginPage.login('user@example.com', 'password');
|
|
391
|
+
|
|
392
|
+
await expect(page).toHaveURL('/dashboard');
|
|
393
|
+
});
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Test Organization
|
|
397
|
+
|
|
398
|
+
### File Structure
|
|
399
|
+
|
|
400
|
+
```
|
|
401
|
+
src/
|
|
402
|
+
├── components/
|
|
403
|
+
│ ├── Button/
|
|
404
|
+
│ │ ├── Button.tsx
|
|
405
|
+
│ │ ├── Button.test.tsx
|
|
406
|
+
│ │ └── Button.stories.tsx
|
|
407
|
+
│ └── Form/
|
|
408
|
+
│ ├── Form.tsx
|
|
409
|
+
│ └── Form.test.tsx
|
|
410
|
+
├── services/
|
|
411
|
+
│ ├── userService.ts
|
|
412
|
+
│ └── userService.test.ts
|
|
413
|
+
└── utils/
|
|
414
|
+
├── validation.ts
|
|
415
|
+
└── validation.test.ts
|
|
416
|
+
|
|
417
|
+
tests/
|
|
418
|
+
├── integration/
|
|
419
|
+
│ └── api.test.ts
|
|
420
|
+
└── e2e/
|
|
421
|
+
├── fixtures/
|
|
422
|
+
├── pages/
|
|
423
|
+
└── specs/
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Test Setup
|
|
427
|
+
|
|
428
|
+
```javascript
|
|
429
|
+
// vitest.config.ts
|
|
430
|
+
import { defineConfig } from 'vitest/config';
|
|
431
|
+
|
|
432
|
+
export default defineConfig({
|
|
433
|
+
test: {
|
|
434
|
+
globals: true,
|
|
435
|
+
environment: 'jsdom',
|
|
436
|
+
setupFiles: ['./tests/setup.ts'],
|
|
437
|
+
coverage: {
|
|
438
|
+
reporter: ['text', 'html'],
|
|
439
|
+
exclude: ['node_modules/', 'tests/'],
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
```javascript
|
|
446
|
+
// tests/setup.ts
|
|
447
|
+
import '@testing-library/jest-dom';
|
|
448
|
+
import { beforeAll, afterAll, afterEach } from 'vitest';
|
|
449
|
+
import { server } from './mocks/server';
|
|
450
|
+
|
|
451
|
+
beforeAll(() => server.listen());
|
|
452
|
+
afterEach(() => server.resetHandlers());
|
|
453
|
+
afterAll(() => server.close());
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
## TDD Workflow
|
|
457
|
+
|
|
458
|
+
### Red-Green-Refactor
|
|
459
|
+
|
|
460
|
+
```javascript
|
|
461
|
+
// 1. RED: Write failing test
|
|
462
|
+
it('returns true for valid email', () => {
|
|
463
|
+
expect(isValidEmail('test@example.com')).toBe(true);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// 2. GREEN: Implement minimal code to pass
|
|
467
|
+
function isValidEmail(email) {
|
|
468
|
+
return email.includes('@');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// 3. REFACTOR: Improve implementation
|
|
472
|
+
function isValidEmail(email) {
|
|
473
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
474
|
+
return emailRegex.test(email);
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
## Testing Checklist
|
|
479
|
+
|
|
480
|
+
### Unit Tests
|
|
481
|
+
- [ ] Pure functions tested
|
|
482
|
+
- [ ] Edge cases covered
|
|
483
|
+
- [ ] Error scenarios tested
|
|
484
|
+
- [ ] Mocks used appropriately
|
|
485
|
+
|
|
486
|
+
### Integration Tests
|
|
487
|
+
- [ ] API endpoints tested
|
|
488
|
+
- [ ] Database operations verified
|
|
489
|
+
- [ ] External services mocked
|
|
490
|
+
|
|
491
|
+
### E2E Tests
|
|
492
|
+
- [ ] Critical user flows covered
|
|
493
|
+
- [ ] Cross-browser testing
|
|
494
|
+
- [ ] Mobile viewports tested
|
|
495
|
+
- [ ] Accessibility checked
|
|
496
|
+
|
|
497
|
+
### General
|
|
498
|
+
- [ ] Naming conventions followed
|
|
499
|
+
- [ ] Tests are independent
|
|
500
|
+
- [ ] Setup/teardown implemented
|
|
501
|
+
- [ ] Coverage targets met
|