910 lines
21 KiB
Markdown
910 lines
21 KiB
Markdown
---
|
|
name: testing-strategy
|
|
description: Comprehensive testing strategies including test pyramid, TDD methodology, testing patterns, coverage goals, and CI/CD integration. Use when writing tests, implementing TDD, reviewing test coverage, debugging test failures, or setting up testing infrastructure.
|
|
---
|
|
|
|
# Testing Strategy
|
|
|
|
This skill provides comprehensive guidance for implementing effective testing strategies across your entire application stack.
|
|
|
|
## Test Pyramid
|
|
|
|
### The Testing Hierarchy
|
|
|
|
```
|
|
/\
|
|
/ \
|
|
/E2E \ 10% - End-to-End Tests (slowest, most expensive)
|
|
/______\
|
|
/ \
|
|
/Integration\ 20% - Integration Tests (medium speed/cost)
|
|
/____________\
|
|
/ \
|
|
/ Unit Tests \ 70% - Unit Tests (fast, cheap, focused)
|
|
/__________________\
|
|
```
|
|
|
|
**Rationale**:
|
|
- **70% Unit Tests**: Fast, isolated, catch bugs early
|
|
- **20% Integration Tests**: Test component interactions
|
|
- **10% E2E Tests**: Test critical user journeys
|
|
|
|
### Why This Distribution?
|
|
|
|
**Unit tests are cheap**:
|
|
- Run in milliseconds
|
|
- No external dependencies
|
|
- Easy to debug
|
|
- High code coverage per test
|
|
|
|
**Integration tests are moderate**:
|
|
- Test real interactions
|
|
- Catch integration bugs
|
|
- Slower than unit tests
|
|
- More complex setup
|
|
|
|
**E2E tests are expensive**:
|
|
- Test entire system
|
|
- Catch UX issues
|
|
- Very slow (seconds/minutes)
|
|
- Brittle and hard to maintain
|
|
|
|
## TDD (Test-Driven Development)
|
|
|
|
### Red-Green-Refactor Cycle
|
|
|
|
**1. Red - Write a failing test**:
|
|
```typescript
|
|
describe('Calculator', () => {
|
|
test('adds two numbers', () => {
|
|
const calculator = new Calculator();
|
|
expect(calculator.add(2, 3)).toBe(5); // FAILS - method doesn't exist
|
|
});
|
|
});
|
|
```
|
|
|
|
**2. Green - Write minimal code to pass**:
|
|
```typescript
|
|
class Calculator {
|
|
add(a: number, b: number): number {
|
|
return a + b; // Simplest implementation
|
|
}
|
|
}
|
|
// Test now PASSES
|
|
```
|
|
|
|
**3. Refactor - Improve the code**:
|
|
```typescript
|
|
class Calculator {
|
|
add(a: number, b: number): number {
|
|
// Add validation
|
|
if (!Number.isFinite(a) || !Number.isFinite(b)) {
|
|
throw new Error('Arguments must be finite numbers');
|
|
}
|
|
return a + b;
|
|
}
|
|
}
|
|
```
|
|
|
|
### TDD Benefits
|
|
|
|
**Design benefits**:
|
|
- Forces you to think about API before implementation
|
|
- Leads to more testable, modular code
|
|
- Encourages SOLID principles
|
|
|
|
**Quality benefits**:
|
|
- 100% test coverage by design
|
|
- Catches bugs immediately
|
|
- Provides living documentation
|
|
|
|
**Workflow benefits**:
|
|
- Clear next step (make test pass)
|
|
- Confidence when refactoring
|
|
- Prevents over-engineering
|
|
|
|
## Arrange-Act-Assert Pattern
|
|
|
|
### The AAA Pattern
|
|
|
|
Every test should follow this structure:
|
|
|
|
```typescript
|
|
test('user registration creates account and sends welcome email', async () => {
|
|
// ARRANGE - Set up test conditions
|
|
const userData = {
|
|
email: 'test@example.com',
|
|
password: 'SecurePass123',
|
|
name: 'Test User',
|
|
};
|
|
const mockEmailService = jest.fn();
|
|
const userService = new UserService(mockEmailService);
|
|
|
|
// ACT - Execute the behavior being tested
|
|
const result = await userService.register(userData);
|
|
|
|
// ASSERT - Verify the outcome
|
|
expect(result.id).toBeDefined();
|
|
expect(result.email).toBe(userData.email);
|
|
expect(mockEmailService).toHaveBeenCalledWith({
|
|
to: userData.email,
|
|
subject: 'Welcome!',
|
|
template: 'welcome',
|
|
});
|
|
});
|
|
```
|
|
|
|
### Why AAA?
|
|
|
|
- **Clear structure**: Easy to understand what's being tested
|
|
- **Consistent**: All tests follow same pattern
|
|
- **Maintainable**: Easy to modify and debug
|
|
|
|
## Mocking Strategies
|
|
|
|
### When to Mock
|
|
|
|
**✅ DO mock**:
|
|
- External APIs
|
|
- Databases
|
|
- File system operations
|
|
- Time/dates
|
|
- Random number generators
|
|
- Network requests
|
|
- Third-party services
|
|
|
|
```typescript
|
|
// Mock external API
|
|
jest.mock('axios');
|
|
|
|
test('fetches user data from API', async () => {
|
|
const mockData = { id: 1, name: 'John' };
|
|
(axios.get as jest.Mock).mockResolvedValue({ data: mockData });
|
|
|
|
const user = await fetchUser(1);
|
|
|
|
expect(user).toEqual(mockData);
|
|
});
|
|
```
|
|
|
|
### When NOT to Mock
|
|
|
|
**❌ DON'T mock**:
|
|
- Pure functions (test them directly)
|
|
- Simple utility functions
|
|
- Domain logic
|
|
- Value objects
|
|
- Internal implementation details
|
|
|
|
```typescript
|
|
// ❌ BAD - Over-mocking
|
|
test('validates email', () => {
|
|
const validator = new EmailValidator();
|
|
jest.spyOn(validator, 'isValid').mockReturnValue(true);
|
|
expect(validator.isValid('test@example.com')).toBe(true);
|
|
// This test is useless - you're testing the mock, not the code
|
|
});
|
|
|
|
// ✅ GOOD - Test real implementation
|
|
test('validates email', () => {
|
|
const validator = new EmailValidator();
|
|
expect(validator.isValid('test@example.com')).toBe(true);
|
|
expect(validator.isValid('invalid')).toBe(false);
|
|
});
|
|
```
|
|
|
|
### Mocking Patterns
|
|
|
|
**Stub** (return predetermined values):
|
|
```typescript
|
|
const mockDatabase = {
|
|
findUser: jest.fn().mockResolvedValue({ id: 1, name: 'John' }),
|
|
saveUser: jest.fn().mockResolvedValue(true),
|
|
};
|
|
```
|
|
|
|
**Spy** (track calls, use real implementation):
|
|
```typescript
|
|
const emailService = new EmailService();
|
|
const sendSpy = jest.spyOn(emailService, 'send');
|
|
|
|
await emailService.send('test@example.com', 'Hello');
|
|
|
|
expect(sendSpy).toHaveBeenCalledTimes(1);
|
|
expect(sendSpy).toHaveBeenCalledWith('test@example.com', 'Hello');
|
|
```
|
|
|
|
**Fake** (lightweight implementation):
|
|
```typescript
|
|
class FakeDatabase {
|
|
private data = new Map();
|
|
|
|
async save(key: string, value: any) {
|
|
this.data.set(key, value);
|
|
}
|
|
|
|
async get(key: string) {
|
|
return this.data.get(key);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Test Coverage Goals
|
|
|
|
### Coverage Metrics
|
|
|
|
**Line Coverage**: Percentage of code lines executed
|
|
- **Target**: 80-90% for critical paths
|
|
|
|
**Branch Coverage**: Percentage of if/else branches tested
|
|
- **Target**: 80%+ (more important than line coverage)
|
|
|
|
**Function Coverage**: Percentage of functions called
|
|
- **Target**: 90%+
|
|
|
|
**Statement Coverage**: Percentage of statements executed
|
|
- **Target**: 80%+
|
|
|
|
### Coverage Configuration
|
|
|
|
```json
|
|
// package.json
|
|
{
|
|
"jest": {
|
|
"collectCoverage": true,
|
|
"coverageThreshold": {
|
|
"global": {
|
|
"branches": 80,
|
|
"functions": 90,
|
|
"lines": 80,
|
|
"statements": 80
|
|
},
|
|
"./src/critical/": {
|
|
"branches": 95,
|
|
"functions": 95,
|
|
"lines": 95,
|
|
"statements": 95
|
|
}
|
|
},
|
|
"coveragePathIgnorePatterns": [
|
|
"/node_modules/",
|
|
"/tests/",
|
|
"/migrations/",
|
|
"/.config.ts$/"
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### What to Prioritize
|
|
|
|
**High priority** (aim for 95%+ coverage):
|
|
- Business logic
|
|
- Security-critical code
|
|
- Payment/billing code
|
|
- Data validation
|
|
- Authentication/authorization
|
|
|
|
**Medium priority** (aim for 80%+ coverage):
|
|
- API endpoints
|
|
- Database queries
|
|
- Utility functions
|
|
- Error handling
|
|
|
|
**Low priority** (optional coverage):
|
|
- UI components (use integration tests instead)
|
|
- Configuration files
|
|
- Type definitions
|
|
- Third-party library wrappers
|
|
|
|
## Integration Testing
|
|
|
|
### Database Integration Tests
|
|
|
|
```typescript
|
|
import { PrismaClient } from '@prisma/client';
|
|
|
|
describe('UserRepository', () => {
|
|
let prisma: PrismaClient;
|
|
let repository: UserRepository;
|
|
|
|
beforeAll(async () => {
|
|
// Use test database
|
|
prisma = new PrismaClient({
|
|
datasources: { db: { url: process.env.TEST_DATABASE_URL } },
|
|
});
|
|
repository = new UserRepository(prisma);
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// Clean database before each test
|
|
await prisma.user.deleteMany();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await prisma.$disconnect();
|
|
});
|
|
|
|
test('creates user and retrieves by email', async () => {
|
|
// ARRANGE
|
|
const userData = {
|
|
email: 'test@example.com',
|
|
name: 'Test User',
|
|
password: 'hashed_password',
|
|
};
|
|
|
|
// ACT
|
|
const created = await repository.create(userData);
|
|
const retrieved = await repository.findByEmail(userData.email);
|
|
|
|
// ASSERT
|
|
expect(retrieved).toBeDefined();
|
|
expect(retrieved?.id).toBe(created.id);
|
|
expect(retrieved?.email).toBe(userData.email);
|
|
});
|
|
});
|
|
```
|
|
|
|
### API Integration Tests
|
|
|
|
```typescript
|
|
import request from 'supertest';
|
|
import { app } from '../src/app';
|
|
|
|
describe('User API', () => {
|
|
test('POST /api/users creates user and returns 201', async () => {
|
|
const response = await request(app)
|
|
.post('/api/users')
|
|
.send({
|
|
email: 'test@example.com',
|
|
password: 'SecurePass123',
|
|
name: 'Test User',
|
|
})
|
|
.expect(201);
|
|
|
|
expect(response.body).toMatchObject({
|
|
email: 'test@example.com',
|
|
name: 'Test User',
|
|
});
|
|
expect(response.body.password).toBeUndefined(); // Never return password
|
|
});
|
|
|
|
test('POST /api/users returns 400 for invalid email', async () => {
|
|
const response = await request(app)
|
|
.post('/api/users')
|
|
.send({
|
|
email: 'invalid-email',
|
|
password: 'SecurePass123',
|
|
name: 'Test User',
|
|
})
|
|
.expect(400);
|
|
|
|
expect(response.body.error.code).toBe('VALIDATION_ERROR');
|
|
});
|
|
});
|
|
```
|
|
|
|
### Service Integration Tests
|
|
|
|
```typescript
|
|
describe('OrderService Integration', () => {
|
|
test('complete order flow', async () => {
|
|
// Create order
|
|
const order = await orderService.create({
|
|
userId: 'user_123',
|
|
items: [{ productId: 'prod_1', quantity: 2 }],
|
|
});
|
|
|
|
// Process payment
|
|
const payment = await paymentService.process({
|
|
orderId: order.id,
|
|
amount: order.total,
|
|
});
|
|
|
|
// Verify inventory updated
|
|
const product = await inventoryService.getProduct('prod_1');
|
|
expect(product.stock).toBe(originalStock - 2);
|
|
|
|
// Verify order status updated
|
|
const updatedOrder = await orderService.getById(order.id);
|
|
expect(updatedOrder.status).toBe('paid');
|
|
});
|
|
});
|
|
```
|
|
|
|
## E2E Testing
|
|
|
|
### Playwright Setup
|
|
|
|
```typescript
|
|
// playwright.config.ts
|
|
import { defineConfig } from '@playwright/test';
|
|
|
|
export default defineConfig({
|
|
testDir: './e2e',
|
|
fullyParallel: true,
|
|
retries: process.env.CI ? 2 : 0,
|
|
workers: process.env.CI ? 1 : undefined,
|
|
use: {
|
|
baseURL: 'http://localhost:3000',
|
|
trace: 'on-first-retry',
|
|
screenshot: 'only-on-failure',
|
|
},
|
|
projects: [
|
|
{ name: 'chromium', use: { browserName: 'chromium' } },
|
|
{ name: 'firefox', use: { browserName: 'firefox' } },
|
|
{ name: 'webkit', use: { browserName: 'webkit' } },
|
|
],
|
|
});
|
|
```
|
|
|
|
### E2E Test Example
|
|
|
|
```typescript
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
test.describe('User Registration Flow', () => {
|
|
test('user can register and login', async ({ page }) => {
|
|
// Navigate to registration page
|
|
await page.goto('/register');
|
|
|
|
// Fill registration form
|
|
await page.fill('[name="email"]', 'test@example.com');
|
|
await page.fill('[name="password"]', 'SecurePass123');
|
|
await page.fill('[name="confirmPassword"]', 'SecurePass123');
|
|
await page.fill('[name="name"]', 'Test User');
|
|
|
|
// Submit form
|
|
await page.click('button[type="submit"]');
|
|
|
|
// Wait for redirect to dashboard
|
|
await page.waitForURL('/dashboard');
|
|
|
|
// Verify welcome message
|
|
await expect(page.locator('h1')).toContainText('Welcome, Test User');
|
|
});
|
|
|
|
test('shows validation errors for invalid input', async ({ page }) => {
|
|
await page.goto('/register');
|
|
|
|
await page.fill('[name="email"]', 'invalid-email');
|
|
await page.fill('[name="password"]', '123'); // Too short
|
|
|
|
await page.click('button[type="submit"]');
|
|
|
|
// Verify error messages displayed
|
|
await expect(page.locator('[data-testid="email-error"]'))
|
|
.toContainText('Invalid email');
|
|
await expect(page.locator('[data-testid="password-error"]'))
|
|
.toContainText('at least 8 characters');
|
|
});
|
|
});
|
|
```
|
|
|
|
### Critical E2E Scenarios
|
|
|
|
Test these critical user journeys:
|
|
- User registration and login
|
|
- Checkout and payment flow
|
|
- Password reset
|
|
- Profile updates
|
|
- Critical business workflows
|
|
|
|
## Performance Testing
|
|
|
|
### Load Testing with k6
|
|
|
|
```javascript
|
|
import http from 'k6/http';
|
|
import { check, sleep } from 'k6';
|
|
|
|
export const options = {
|
|
stages: [
|
|
{ duration: '30s', target: 20 }, // Ramp up to 20 users
|
|
{ duration: '1m', target: 20 }, // Stay at 20 users
|
|
{ duration: '30s', target: 100 }, // Ramp up to 100 users
|
|
{ duration: '1m', target: 100 }, // Stay at 100 users
|
|
{ duration: '30s', target: 0 }, // Ramp down to 0 users
|
|
],
|
|
thresholds: {
|
|
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
|
|
http_req_failed: ['rate<0.01'], // Less than 1% error rate
|
|
},
|
|
};
|
|
|
|
export default function() {
|
|
const response = http.get('https://api.example.com/users');
|
|
|
|
check(response, {
|
|
'status is 200': (r) => r.status === 200,
|
|
'response time < 500ms': (r) => r.timings.duration < 500,
|
|
});
|
|
|
|
sleep(1);
|
|
}
|
|
```
|
|
|
|
### Benchmark Testing
|
|
|
|
```typescript
|
|
import { performance } from 'perf_hooks';
|
|
|
|
describe('Performance Benchmarks', () => {
|
|
test('database query completes in under 100ms', async () => {
|
|
const start = performance.now();
|
|
|
|
await database.query('SELECT * FROM users WHERE email = ?', ['test@example.com']);
|
|
|
|
const duration = performance.now() - start;
|
|
expect(duration).toBeLessThan(100);
|
|
});
|
|
|
|
test('API endpoint responds in under 200ms', async () => {
|
|
const start = performance.now();
|
|
|
|
await request(app).get('/api/users/123');
|
|
|
|
const duration = performance.now() - start;
|
|
expect(duration).toBeLessThan(200);
|
|
});
|
|
});
|
|
```
|
|
|
|
## Flaky Test Prevention
|
|
|
|
### Common Causes of Flaky Tests
|
|
|
|
**1. Race Conditions**:
|
|
```typescript
|
|
// ❌ BAD - Race condition
|
|
test('displays data', async () => {
|
|
fetchData();
|
|
expect(screen.getByText('Data loaded')).toBeInTheDocument();
|
|
// Fails intermittently if fetchData takes longer than expected
|
|
});
|
|
|
|
// ✅ GOOD - Wait for async operation
|
|
test('displays data', async () => {
|
|
fetchData();
|
|
await screen.findByText('Data loaded'); // Waits up to 1 second
|
|
});
|
|
```
|
|
|
|
**2. Time Dependencies**:
|
|
```typescript
|
|
// ❌ BAD - Depends on current time
|
|
test('shows message for new users', () => {
|
|
const user = { createdAt: new Date() };
|
|
expect(isNewUser(user)).toBe(true);
|
|
// Fails if test runs slowly
|
|
});
|
|
|
|
// ✅ GOOD - Mock time
|
|
test('shows message for new users', () => {
|
|
jest.useFakeTimers();
|
|
jest.setSystemTime(new Date('2025-10-16'));
|
|
|
|
const user = { createdAt: new Date('2025-10-15') };
|
|
expect(isNewUser(user)).toBe(true);
|
|
|
|
jest.useRealTimers();
|
|
});
|
|
```
|
|
|
|
**3. Shared State**:
|
|
```typescript
|
|
// ❌ BAD - Tests share state
|
|
let counter = 0;
|
|
|
|
test('increments counter', () => {
|
|
counter++;
|
|
expect(counter).toBe(1);
|
|
});
|
|
|
|
test('increments counter again', () => {
|
|
counter++;
|
|
expect(counter).toBe(1); // Fails if first test ran
|
|
});
|
|
|
|
// ✅ GOOD - Isolated state
|
|
test('increments counter', () => {
|
|
const counter = new Counter();
|
|
counter.increment();
|
|
expect(counter.value).toBe(1);
|
|
});
|
|
```
|
|
|
|
### Flaky Test Best Practices
|
|
|
|
1. **Always clean up after tests**:
|
|
```typescript
|
|
afterEach(async () => {
|
|
await database.truncate();
|
|
jest.clearAllMocks();
|
|
jest.useRealTimers();
|
|
});
|
|
```
|
|
|
|
2. **Use explicit waits, not delays**:
|
|
```typescript
|
|
// ❌ BAD
|
|
await sleep(1000);
|
|
|
|
// ✅ GOOD
|
|
await waitFor(() => expect(element).toBeInTheDocument());
|
|
```
|
|
|
|
3. **Isolate test data**:
|
|
```typescript
|
|
test('creates user', async () => {
|
|
const uniqueEmail = `test-${Date.now()}@example.com`;
|
|
const user = await createUser({ email: uniqueEmail });
|
|
expect(user.email).toBe(uniqueEmail);
|
|
});
|
|
```
|
|
|
|
## Test Data Management
|
|
|
|
### Test Fixtures
|
|
|
|
```typescript
|
|
// fixtures/users.ts
|
|
export const testUsers = {
|
|
admin: {
|
|
email: 'admin@example.com',
|
|
password: 'AdminPass123',
|
|
role: 'admin',
|
|
},
|
|
regular: {
|
|
email: 'user@example.com',
|
|
password: 'UserPass123',
|
|
role: 'user',
|
|
},
|
|
};
|
|
|
|
// Usage in tests
|
|
import { testUsers } from './fixtures/users';
|
|
|
|
test('admin can delete users', async () => {
|
|
const admin = await createUser(testUsers.admin);
|
|
// Test admin functionality
|
|
});
|
|
```
|
|
|
|
### Factory Pattern
|
|
|
|
```typescript
|
|
class UserFactory {
|
|
static create(overrides = {}) {
|
|
return {
|
|
id: faker.datatype.uuid(),
|
|
email: faker.internet.email(),
|
|
name: faker.name.fullName(),
|
|
createdAt: new Date(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
static createMany(count: number, overrides = {}) {
|
|
return Array.from({ length: count }, () => this.create(overrides));
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
test('displays user list', () => {
|
|
const users = UserFactory.createMany(5);
|
|
render(<UserList users={users} />);
|
|
expect(screen.getAllByRole('listitem')).toHaveLength(5);
|
|
});
|
|
```
|
|
|
|
### Database Seeding
|
|
|
|
```typescript
|
|
// seeds/test-seed.ts
|
|
export async function seedTestDatabase() {
|
|
// Create admin user
|
|
const admin = await prisma.user.create({
|
|
data: { email: 'admin@test.com', role: 'admin' },
|
|
});
|
|
|
|
// Create test products
|
|
const products = await Promise.all([
|
|
prisma.product.create({ data: { name: 'Product 1', price: 10 } }),
|
|
prisma.product.create({ data: { name: 'Product 2', price: 20 } }),
|
|
]);
|
|
|
|
return { admin, products };
|
|
}
|
|
|
|
// Usage
|
|
beforeEach(async () => {
|
|
await prisma.$executeRaw`TRUNCATE TABLE users CASCADE`;
|
|
const { admin, products } = await seedTestDatabase();
|
|
});
|
|
```
|
|
|
|
## CI/CD Integration
|
|
|
|
### GitHub Actions Configuration
|
|
|
|
```yaml
|
|
# .github/workflows/test.yml
|
|
name: Tests
|
|
|
|
on:
|
|
push:
|
|
branches: [main, develop]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
|
|
services:
|
|
postgres:
|
|
image: postgres:15
|
|
env:
|
|
POSTGRES_PASSWORD: postgres
|
|
options: >-
|
|
--health-cmd pg_isready
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v3
|
|
with:
|
|
node-version: '18'
|
|
cache: 'npm'
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Run linter
|
|
run: npm run lint
|
|
|
|
- name: Run type check
|
|
run: npm run type-check
|
|
|
|
- name: Run unit tests
|
|
run: npm run test:unit
|
|
|
|
- name: Run integration tests
|
|
run: npm run test:integration
|
|
env:
|
|
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
|
|
|
|
- name: Run E2E tests
|
|
run: npm run test:e2e
|
|
|
|
- name: Upload coverage
|
|
uses: codecov/codecov-action@v3
|
|
with:
|
|
files: ./coverage/coverage-final.json
|
|
fail_ci_if_error: true
|
|
```
|
|
|
|
### Test Scripts Organization
|
|
|
|
```json
|
|
// package.json
|
|
{
|
|
"scripts": {
|
|
"test": "npm run test:unit && npm run test:integration && npm run test:e2e",
|
|
"test:unit": "jest --testPathPattern=\\.test\\.ts$",
|
|
"test:integration": "jest --testPathPattern=\\.integration\\.ts$",
|
|
"test:e2e": "playwright test",
|
|
"test:watch": "jest --watch",
|
|
"test:coverage": "jest --coverage",
|
|
"test:ci": "jest --ci --coverage --maxWorkers=2"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Test Performance in CI
|
|
|
|
**Parallel execution**:
|
|
```yaml
|
|
jobs:
|
|
test:
|
|
strategy:
|
|
matrix:
|
|
shard: [1, 2, 3, 4]
|
|
steps:
|
|
- run: npm test -- --shard=${{ matrix.shard }}/4
|
|
```
|
|
|
|
**Cache dependencies**:
|
|
```yaml
|
|
- uses: actions/cache@v3
|
|
with:
|
|
path: ~/.npm
|
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
```
|
|
|
|
## Test Organization
|
|
|
|
### File Structure
|
|
|
|
```
|
|
tests/
|
|
├── unit/ # Fast, isolated tests
|
|
│ ├── services/
|
|
│ │ ├── user-service.test.ts
|
|
│ │ └── order-service.test.ts
|
|
│ └── utils/
|
|
│ ├── validator.test.ts
|
|
│ └── formatter.test.ts
|
|
├── integration/ # Database, API tests
|
|
│ ├── api/
|
|
│ │ ├── users.integration.ts
|
|
│ │ └── orders.integration.ts
|
|
│ └── database/
|
|
│ └── repository.integration.ts
|
|
├── e2e/ # End-to-end tests
|
|
│ ├── auth.spec.ts
|
|
│ ├── checkout.spec.ts
|
|
│ └── profile.spec.ts
|
|
├── fixtures/ # Test data
|
|
│ ├── users.ts
|
|
│ └── products.ts
|
|
└── helpers/ # Test utilities
|
|
├── setup.ts
|
|
└── factories.ts
|
|
```
|
|
|
|
### Test Naming Conventions
|
|
|
|
```typescript
|
|
// Pattern: describe('Component/Function', () => test('should...when...'))
|
|
|
|
describe('UserService', () => {
|
|
describe('register', () => {
|
|
test('should create user when valid data provided', async () => {
|
|
// Test implementation
|
|
});
|
|
|
|
test('should throw error when email already exists', async () => {
|
|
// Test implementation
|
|
});
|
|
|
|
test('should hash password before saving', async () => {
|
|
// Test implementation
|
|
});
|
|
});
|
|
|
|
describe('login', () => {
|
|
test('should return token when credentials are valid', async () => {
|
|
// Test implementation
|
|
});
|
|
|
|
test('should throw error when password is incorrect', async () => {
|
|
// Test implementation
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
## When to Use This Skill
|
|
|
|
Use this skill when:
|
|
- Setting up testing infrastructure
|
|
- Writing unit, integration, or E2E tests
|
|
- Implementing TDD methodology
|
|
- Reviewing test coverage
|
|
- Debugging flaky tests
|
|
- Optimizing test performance
|
|
- Configuring CI/CD pipelines
|
|
- Establishing testing standards
|
|
- Training team on testing practices
|
|
- Improving code quality through testing
|
|
|
|
---
|
|
|
|
**Remember**: Good tests give you confidence to refactor, catch bugs early, and serve as living documentation. Invest in your test suite and it will pay dividends throughout the project lifecycle.
|