Initial commit
This commit is contained in:
769
skills/e2e-playwright/SKILL.md
Normal file
769
skills/e2e-playwright/SKILL.md
Normal file
@@ -0,0 +1,769 @@
|
||||
---
|
||||
name: e2e-playwright
|
||||
description: Comprehensive Playwright end-to-end testing expertise covering browser automation, cross-browser testing, visual regression, API testing, mobile emulation, accessibility testing, test architecture, page object models, fixtures, parallel execution, CI/CD integration, debugging strategies, and production-grade E2E test patterns. Activates for playwright, e2e testing, end-to-end testing, browser automation, cross-browser testing, visual testing, screenshot testing, API testing, mobile testing, accessibility testing, test fixtures, page object model, POM, test architecture, parallel testing, playwright config, trace viewer, codegen, test debugging, flaky tests, CI integration, playwright best practices.
|
||||
---
|
||||
|
||||
# E2E Playwright Testing Expert
|
||||
|
||||
## Core Expertise
|
||||
|
||||
### 1. Playwright Fundamentals
|
||||
**Browser Automation**:
|
||||
- Multi-browser support (Chromium, Firefox, WebKit)
|
||||
- Context isolation and parallel execution
|
||||
- Auto-waiting and actionability checks
|
||||
- Network interception and mocking
|
||||
- File downloads and uploads
|
||||
- Geolocation and permissions
|
||||
- Authentication state management
|
||||
|
||||
**Test Structure**:
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Authentication Flow', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
});
|
||||
|
||||
test('should login successfully', async ({ page }) => {
|
||||
await page.getByLabel('Email').fill('user@example.com');
|
||||
await page.getByLabel('Password').fill('password123');
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
await expect(page.getByText('Welcome back')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show validation errors', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
|
||||
await expect(page.getByText('Email is required')).toBeVisible();
|
||||
await expect(page.getByText('Password is required')).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Page Object Model (POM)
|
||||
**Pattern**: Encapsulate page interactions for maintainability
|
||||
|
||||
```typescript
|
||||
// pages/LoginPage.ts
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
export class LoginPage {
|
||||
readonly page: Page;
|
||||
readonly emailInput: Locator;
|
||||
readonly passwordInput: Locator;
|
||||
readonly loginButton: Locator;
|
||||
readonly errorMessage: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.emailInput = page.getByLabel('Email');
|
||||
this.passwordInput = page.getByLabel('Password');
|
||||
this.loginButton = page.getByRole('button', { name: 'Login' });
|
||||
this.errorMessage = page.getByRole('alert');
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/login');
|
||||
}
|
||||
|
||||
async login(email: string, password: string) {
|
||||
await this.emailInput.fill(email);
|
||||
await this.passwordInput.fill(password);
|
||||
await this.loginButton.click();
|
||||
}
|
||||
|
||||
async loginWithGoogle() {
|
||||
await this.page.getByRole('button', { name: 'Continue with Google' }).click();
|
||||
// Handle OAuth popup
|
||||
}
|
||||
|
||||
async expectError(message: string) {
|
||||
await expect(this.errorMessage).toContainText(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in tests
|
||||
test('login flow', async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
await loginPage.login('user@example.com', 'password123');
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Test Fixtures & Custom Contexts
|
||||
**Fixtures**: Reusable setup/teardown logic
|
||||
|
||||
```typescript
|
||||
// fixtures/auth.fixture.ts
|
||||
import { test as base } from '@playwright/test';
|
||||
import { LoginPage } from '../pages/LoginPage';
|
||||
|
||||
type AuthFixtures = {
|
||||
authenticatedPage: Page;
|
||||
loginPage: LoginPage;
|
||||
};
|
||||
|
||||
export const test = base.extend<AuthFixtures>({
|
||||
authenticatedPage: async ({ page }, use) => {
|
||||
// Setup: Login before test
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('Email').fill('user@example.com');
|
||||
await page.getByLabel('Password').fill('password123');
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
await page.waitForURL('/dashboard');
|
||||
|
||||
await use(page);
|
||||
|
||||
// Teardown: Logout after test
|
||||
await page.getByRole('button', { name: 'Logout' }).click();
|
||||
},
|
||||
|
||||
loginPage: async ({ page }, use) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
await use(loginPage);
|
||||
},
|
||||
});
|
||||
|
||||
export { expect } from '@playwright/test';
|
||||
|
||||
// Usage
|
||||
test('authenticated user can view profile', async ({ authenticatedPage }) => {
|
||||
await authenticatedPage.goto('/profile');
|
||||
await expect(authenticatedPage.getByText('Profile Settings')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
### 4. API Testing with Playwright
|
||||
**Pattern**: Test backend APIs alongside E2E flows
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('API Testing', () => {
|
||||
test('should fetch user data', async ({ request }) => {
|
||||
const response = await request.get('/api/users/123');
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data).toMatchObject({
|
||||
id: 123,
|
||||
email: expect.any(String),
|
||||
name: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle authentication', async ({ request }) => {
|
||||
const response = await request.post('/api/auth/login', {
|
||||
data: {
|
||||
email: 'user@example.com',
|
||||
password: 'password123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const { token } = await response.json();
|
||||
expect(token).toBeTruthy();
|
||||
|
||||
// Use token in subsequent requests
|
||||
const profileResponse = await request.get('/api/profile', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(profileResponse.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should mock API responses', async ({ page }) => {
|
||||
await page.route('/api/users', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify([
|
||||
{ id: 1, name: 'John Doe' },
|
||||
{ id: 2, name: 'Jane Smith' },
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/users');
|
||||
await expect(page.getByText('John Doe')).toBeVisible();
|
||||
await expect(page.getByText('Jane Smith')).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Visual Regression Testing
|
||||
**Pattern**: Screenshot comparison for UI changes
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Visual Regression', () => {
|
||||
test('homepage matches baseline', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveScreenshot('homepage.png', {
|
||||
fullPage: true,
|
||||
animations: 'disabled',
|
||||
});
|
||||
});
|
||||
|
||||
test('component states', async ({ page }) => {
|
||||
await page.goto('/components');
|
||||
|
||||
// Default state
|
||||
const button = page.getByRole('button', { name: 'Submit' });
|
||||
await expect(button).toHaveScreenshot('button-default.png');
|
||||
|
||||
// Hover state
|
||||
await button.hover();
|
||||
await expect(button).toHaveScreenshot('button-hover.png');
|
||||
|
||||
// Disabled state
|
||||
await page.evaluate(() => {
|
||||
document.querySelector('button')?.setAttribute('disabled', 'true');
|
||||
});
|
||||
await expect(button).toHaveScreenshot('button-disabled.png');
|
||||
});
|
||||
|
||||
test('responsive screenshots', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Desktop
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
await expect(page).toHaveScreenshot('homepage-desktop.png');
|
||||
|
||||
// Tablet
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
await expect(page).toHaveScreenshot('homepage-tablet.png');
|
||||
|
||||
// Mobile
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await expect(page).toHaveScreenshot('homepage-mobile.png');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 6. Mobile Emulation & Device Testing
|
||||
**Pattern**: Test responsive behavior and touch interactions
|
||||
|
||||
```typescript
|
||||
import { test, expect, devices } from '@playwright/test';
|
||||
|
||||
test.use(devices['iPhone 13 Pro']);
|
||||
|
||||
test.describe('Mobile Experience', () => {
|
||||
test('should render mobile navigation', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Mobile menu should be visible
|
||||
await expect(page.getByRole('button', { name: 'Menu' })).toBeVisible();
|
||||
|
||||
// Desktop nav should be hidden
|
||||
await expect(page.getByRole('navigation').first()).toBeHidden();
|
||||
});
|
||||
|
||||
test('touch gestures', async ({ page }) => {
|
||||
await page.goto('/gallery');
|
||||
|
||||
const image = page.getByRole('img').first();
|
||||
|
||||
// Swipe left
|
||||
await image.dispatchEvent('touchstart', { touches: [{ clientX: 300, clientY: 200 }] });
|
||||
await image.dispatchEvent('touchmove', { touches: [{ clientX: 100, clientY: 200 }] });
|
||||
await image.dispatchEvent('touchend');
|
||||
|
||||
await expect(page.getByText('Next Image')).toBeVisible();
|
||||
});
|
||||
|
||||
test('landscape orientation', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 812, height: 375 }); // iPhone landscape
|
||||
await page.goto('/video');
|
||||
|
||||
await expect(page.locator('video')).toHaveCSS('width', '100%');
|
||||
});
|
||||
});
|
||||
|
||||
// Test across multiple devices
|
||||
for (const deviceName of ['iPhone 13', 'Pixel 5', 'iPad Pro']) {
|
||||
test.describe(`Device: ${deviceName}`, () => {
|
||||
test.use(devices[deviceName]);
|
||||
|
||||
test('critical user flow', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
// Test critical flow on each device
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Accessibility Testing
|
||||
**Pattern**: Automated accessibility checks
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
|
||||
test.describe('Accessibility', () => {
|
||||
test('should not have accessibility violations', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
const accessibilityScanResults = await new AxeBuilder({ page })
|
||||
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
||||
.analyze();
|
||||
|
||||
expect(accessibilityScanResults.violations).toEqual([]);
|
||||
});
|
||||
|
||||
test('keyboard navigation', async ({ page }) => {
|
||||
await page.goto('/form');
|
||||
|
||||
// Tab through form fields
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(page.getByLabel('Email')).toBeFocused();
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(page.getByLabel('Password')).toBeFocused();
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(page.getByRole('button', { name: 'Submit' })).toBeFocused();
|
||||
|
||||
// Submit with Enter
|
||||
await page.keyboard.press('Enter');
|
||||
});
|
||||
|
||||
test('screen reader support', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Check ARIA labels
|
||||
await expect(page.getByRole('navigation', { name: 'Main' })).toBeVisible();
|
||||
await expect(page.getByRole('main')).toHaveAttribute('aria-label', 'Main content');
|
||||
|
||||
// Check alt text
|
||||
const images = page.getByRole('img');
|
||||
for (const img of await images.all()) {
|
||||
await expect(img).toHaveAttribute('alt');
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 8. Performance Testing
|
||||
**Pattern**: Monitor performance metrics
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Performance', () => {
|
||||
test('page load performance', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
const performanceMetrics = await page.evaluate(() => {
|
||||
const perfData = window.performance.timing;
|
||||
return {
|
||||
loadTime: perfData.loadEventEnd - perfData.navigationStart,
|
||||
domContentLoaded: perfData.domContentLoadedEventEnd - perfData.navigationStart,
|
||||
firstPaint: performance.getEntriesByType('paint')[0]?.startTime || 0,
|
||||
};
|
||||
});
|
||||
|
||||
expect(performanceMetrics.loadTime).toBeLessThan(3000); // 3s max
|
||||
expect(performanceMetrics.domContentLoaded).toBeLessThan(2000); // 2s max
|
||||
});
|
||||
|
||||
test('Core Web Vitals', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
const vitals = await page.evaluate(() => {
|
||||
return new Promise((resolve) => {
|
||||
new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const lcp = entries.find(e => e.entryType === 'largest-contentful-paint');
|
||||
const fid = entries.find(e => e.entryType === 'first-input');
|
||||
const cls = entries.find(e => e.entryType === 'layout-shift');
|
||||
|
||||
resolve({ lcp: lcp?.startTime, fid: fid?.processingStart, cls: cls?.value });
|
||||
}).observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift'] });
|
||||
});
|
||||
});
|
||||
|
||||
expect(vitals.lcp).toBeLessThan(2500); // Good LCP
|
||||
expect(vitals.fid).toBeLessThan(100); // Good FID
|
||||
expect(vitals.cls).toBeLessThan(0.1); // Good CLS
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 9. Advanced Configuration
|
||||
**playwright.config.ts**:
|
||||
|
||||
```typescript
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: [
|
||||
['html'],
|
||||
['junit', { outputFile: 'test-results/junit.xml' }],
|
||||
['json', { outputFile: 'test-results/results.json' }],
|
||||
],
|
||||
use: {
|
||||
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
projects: [
|
||||
// Desktop browsers
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
// Mobile browsers
|
||||
{
|
||||
name: 'Mobile Chrome',
|
||||
use: { ...devices['Pixel 5'] },
|
||||
},
|
||||
{
|
||||
name: 'Mobile Safari',
|
||||
use: { ...devices['iPhone 13'] },
|
||||
},
|
||||
// Tablet browsers
|
||||
{
|
||||
name: 'iPad',
|
||||
use: { ...devices['iPad Pro'] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120 * 1000,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 10. CI/CD Integration
|
||||
**GitHub Actions**:
|
||||
|
||||
```yaml
|
||||
name: E2E Tests
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Run E2E tests
|
||||
run: npm run test:e2e
|
||||
env:
|
||||
BASE_URL: https://staging.example.com
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload traces
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: playwright-traces
|
||||
path: test-results/
|
||||
```
|
||||
|
||||
### 11. Debugging Strategies
|
||||
**Tools & Techniques**:
|
||||
|
||||
```typescript
|
||||
// 1. Debug mode (headed browser + slow motion)
|
||||
test('debug example', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.pause(); // Pauses execution, opens inspector
|
||||
});
|
||||
|
||||
// 2. Console logs
|
||||
test('capture console', async ({ page }) => {
|
||||
page.on('console', msg => console.log(`Browser: ${msg.text()}`));
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
// 3. Network inspection
|
||||
test('inspect network', async ({ page }) => {
|
||||
page.on('request', request => console.log('Request:', request.url()));
|
||||
page.on('response', response => console.log('Response:', response.status()));
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
// 4. Screenshots on failure
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
if (testInfo.status !== testInfo.expectedStatus) {
|
||||
await page.screenshot({
|
||||
path: `screenshots/${testInfo.title}.png`,
|
||||
fullPage: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 5. Trace viewer
|
||||
// Run: npx playwright test --trace on
|
||||
// View: npx playwright show-trace trace.zip
|
||||
```
|
||||
|
||||
**Common Debugging Commands**:
|
||||
```bash
|
||||
# Run in headed mode (see browser)
|
||||
npx playwright test --headed
|
||||
|
||||
# Run with UI mode (interactive debugging)
|
||||
npx playwright test --ui
|
||||
|
||||
# Run single test
|
||||
npx playwright test tests/login.spec.ts
|
||||
|
||||
# Debug specific test
|
||||
npx playwright test tests/login.spec.ts --debug
|
||||
|
||||
# Generate test code
|
||||
npx playwright codegen http://localhost:3000
|
||||
```
|
||||
|
||||
### 12. Handling Flaky Tests
|
||||
**Patterns for Reliability**:
|
||||
|
||||
```typescript
|
||||
// 1. Proper waiting strategies
|
||||
test('wait for content', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// ❌ BAD: Fixed delays
|
||||
// await page.waitForTimeout(5000);
|
||||
|
||||
// ✅ GOOD: Wait for specific conditions
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('.content', { state: 'visible' });
|
||||
await page.getByText('Welcome').waitFor();
|
||||
});
|
||||
|
||||
// 2. Retry logic for external dependencies
|
||||
test('api with retry', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
let retries = 3;
|
||||
while (retries > 0) {
|
||||
try {
|
||||
const response = await page.waitForResponse(
|
||||
response => response.url().includes('/api/data') && response.ok(),
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
break;
|
||||
} catch (error) {
|
||||
retries--;
|
||||
if (retries === 0) throw error;
|
||||
await page.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Test isolation
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Clear state before each test
|
||||
await page.context().clearCookies();
|
||||
await page.context().clearPermissions();
|
||||
});
|
||||
|
||||
// 4. Deterministic test data
|
||||
test('use fixtures', async ({ page }) => {
|
||||
// Seed database with known data
|
||||
await page.request.post('/api/test/seed', {
|
||||
data: { userId: 'test-123', email: 'test@example.com' }
|
||||
});
|
||||
|
||||
await page.goto('/users/test-123');
|
||||
await expect(page.getByText('test@example.com')).toBeVisible();
|
||||
|
||||
// Cleanup
|
||||
await page.request.delete('/api/test/users/test-123');
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Test Organization
|
||||
```
|
||||
e2e/
|
||||
├── fixtures/
|
||||
│ ├── auth.fixture.ts
|
||||
│ ├── data.fixture.ts
|
||||
│ └── mock.fixture.ts
|
||||
├── pages/
|
||||
│ ├── LoginPage.ts
|
||||
│ ├── DashboardPage.ts
|
||||
│ └── ProfilePage.ts
|
||||
├── tests/
|
||||
│ ├── auth/
|
||||
│ │ ├── login.spec.ts
|
||||
│ │ ├── signup.spec.ts
|
||||
│ │ └── logout.spec.ts
|
||||
│ ├── user/
|
||||
│ │ ├── profile.spec.ts
|
||||
│ │ └── settings.spec.ts
|
||||
│ └── api/
|
||||
│ ├── users.spec.ts
|
||||
│ └── posts.spec.ts
|
||||
└── playwright.config.ts
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
- Test files: `*.spec.ts` or `*.test.ts`
|
||||
- Page objects: `*Page.ts`
|
||||
- Fixtures: `*.fixture.ts`
|
||||
- Descriptive test names: `should allow user to login with valid credentials`
|
||||
|
||||
### Performance Optimization
|
||||
1. **Parallel execution**: Run tests in parallel across workers
|
||||
2. **Test sharding**: Split tests across CI machines
|
||||
3. **Selective testing**: Use tags/annotations for smoke tests
|
||||
4. **Reuse authentication**: Save auth state, reuse across tests
|
||||
5. **Mock external APIs**: Reduce network latency and flakiness
|
||||
|
||||
### Security Considerations
|
||||
- Never commit credentials in test files
|
||||
- Use environment variables for sensitive data
|
||||
- Isolate test data from production
|
||||
- Clear cookies/storage between tests
|
||||
- Use disposable test accounts
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Authentication State Reuse
|
||||
```typescript
|
||||
// global-setup.ts
|
||||
import { chromium, FullConfig } from '@playwright/test';
|
||||
|
||||
async function globalSetup(config: FullConfig) {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.goto('http://localhost:3000/login');
|
||||
await page.getByLabel('Email').fill('user@example.com');
|
||||
await page.getByLabel('Password').fill('password123');
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
await page.waitForURL('http://localhost:3000/dashboard');
|
||||
|
||||
// Save signed-in state
|
||||
await page.context().storageState({ path: 'auth.json' });
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
export default globalSetup;
|
||||
|
||||
// playwright.config.ts
|
||||
export default defineConfig({
|
||||
globalSetup: require.resolve('./global-setup'),
|
||||
use: {
|
||||
storageState: 'auth.json',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Multi-Tab/Window Testing
|
||||
```typescript
|
||||
test('open in new tab', async ({ context }) => {
|
||||
const page = await context.newPage();
|
||||
await page.goto('/');
|
||||
|
||||
const [newPage] = await Promise.all([
|
||||
context.waitForEvent('page'),
|
||||
page.getByRole('link', { name: 'Open in new tab' }).click()
|
||||
]);
|
||||
|
||||
await newPage.waitForLoadState();
|
||||
await expect(newPage).toHaveURL('/new-page');
|
||||
});
|
||||
```
|
||||
|
||||
### File Upload/Download
|
||||
```typescript
|
||||
test('upload file', async ({ page }) => {
|
||||
await page.goto('/upload');
|
||||
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.getByRole('button', { name: 'Upload' }).click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles('path/to/file.pdf');
|
||||
|
||||
await expect(page.getByText('file.pdf uploaded')).toBeVisible();
|
||||
});
|
||||
|
||||
test('download file', async ({ page }) => {
|
||||
await page.goto('/downloads');
|
||||
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByRole('link', { name: 'Download Report' }).click();
|
||||
const download = await downloadPromise;
|
||||
|
||||
await download.saveAs(`/tmp/${download.suggestedFilename()}`);
|
||||
expect(download.suggestedFilename()).toBe('report.pdf');
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
1. **Timeouts**: Increase timeout, use proper wait strategies
|
||||
2. **Flaky selectors**: Use stable locators (roles, labels, test IDs)
|
||||
3. **Race conditions**: Wait for network idle, use explicit waits
|
||||
4. **Authentication failures**: Clear cookies, check auth state
|
||||
5. **Screenshot mismatches**: Update baselines, disable animations
|
||||
|
||||
### Debug Checklist
|
||||
- [ ] Test passes locally in headed mode?
|
||||
- [ ] Network requests succeed (check DevTools)?
|
||||
- [ ] Selectors are stable and unique?
|
||||
- [ ] Proper waits before assertions?
|
||||
- [ ] Test data is deterministic?
|
||||
- [ ] No race conditions with async operations?
|
||||
- [ ] Traces/screenshots captured on failure?
|
||||
|
||||
## Resources
|
||||
- **Official Docs**: https://playwright.dev
|
||||
- **API Reference**: https://playwright.dev/docs/api/class-playwright
|
||||
- **Best Practices**: https://playwright.dev/docs/best-practices
|
||||
- **Examples**: https://github.com/microsoft/playwright/tree/main/examples
|
||||
- **Community**: https://github.com/microsoft/playwright/discussions
|
||||
454
skills/tdd-expert/SKILL.md
Normal file
454
skills/tdd-expert/SKILL.md
Normal file
@@ -0,0 +1,454 @@
|
||||
---
|
||||
name: tdd-expert
|
||||
description: Test-Driven Development (TDD) expertise covering red-green-refactor cycle, behavior-driven development, test-first design, refactoring with confidence, TDD best practices, TDD workflow, unit testing strategies, mock-driven development, test doubles, TDD patterns, SOLID principles through testing, emergent design, incremental development, TDD anti-patterns, and production-grade TDD practices. Activates for TDD, test-driven development, red-green-refactor, test-first, behavior-driven, BDD, refactoring, test doubles, mock-driven, test design, SOLID principles, emergent design, incremental development, TDD workflow, TDD best practices, TDD patterns, Kent Beck, Robert Martin, Uncle Bob, test-first design.
|
||||
---
|
||||
|
||||
# Test-Driven Development (TDD) Expert
|
||||
|
||||
**Self-contained TDD expertise for ANY user project.**
|
||||
|
||||
---
|
||||
|
||||
## The TDD Cycle: Red-Green-Refactor
|
||||
|
||||
### 1. RED Phase: Write Failing Test
|
||||
|
||||
**Goal**: Define expected behavior through a failing test
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Calculator } from './Calculator';
|
||||
|
||||
describe('Calculator', () => {
|
||||
it('should add two numbers', () => {
|
||||
const calculator = new Calculator();
|
||||
expect(calculator.add(2, 3)).toBe(5); // WILL FAIL - Calculator doesn't exist
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**RED Checklist**:
|
||||
- [ ] Test describes ONE specific behavior
|
||||
- [ ] Test fails for RIGHT reason (not syntax error)
|
||||
- [ ] Test name is clear
|
||||
- [ ] Expected behavior obvious
|
||||
|
||||
### 2. GREEN Phase: Minimal Implementation
|
||||
|
||||
**Goal**: Simplest code that makes test pass
|
||||
|
||||
```typescript
|
||||
// Calculator.ts
|
||||
export class Calculator {
|
||||
add(a: number, b: number): number {
|
||||
return a + b; // Minimal implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**GREEN Checklist**:
|
||||
- [ ] Test passes
|
||||
- [ ] Code is simplest possible
|
||||
- [ ] No premature optimization
|
||||
- [ ] No extra features
|
||||
|
||||
### 3. REFACTOR Phase: Improve Design
|
||||
|
||||
**Goal**: Improve code quality without changing behavior
|
||||
|
||||
```typescript
|
||||
// Refactor: Support variable arguments
|
||||
export class Calculator {
|
||||
add(...numbers: number[]): number {
|
||||
return numbers.reduce((sum, n) => sum + n, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Tests still pass!
|
||||
```
|
||||
|
||||
**REFACTOR Checklist**:
|
||||
- [ ] All tests still pass
|
||||
- [ ] Code is more readable
|
||||
- [ ] Removed duplication
|
||||
- [ ] Better design patterns
|
||||
|
||||
---
|
||||
|
||||
## TDD Benefits
|
||||
|
||||
**Design Benefits**:
|
||||
- Forces modular, testable code
|
||||
- Reveals design problems early
|
||||
- Encourages SOLID principles
|
||||
- Promotes simple solutions
|
||||
|
||||
**Quality Benefits**:
|
||||
- 100% test coverage (by definition)
|
||||
- Tests document behavior
|
||||
- Regression safety net
|
||||
- Faster debugging
|
||||
|
||||
**Productivity Benefits**:
|
||||
- Less time debugging
|
||||
- Confidence to refactor
|
||||
- Faster iterations
|
||||
- Clearer requirements
|
||||
|
||||
---
|
||||
|
||||
## BDD: Behavior-Driven Development
|
||||
|
||||
**Extension of TDD with natural language tests**
|
||||
|
||||
### Given-When-Then Pattern
|
||||
|
||||
```typescript
|
||||
describe('Shopping Cart', () => {
|
||||
it('should apply 10% discount when total exceeds $100', () => {
|
||||
// Given: A cart with $120 worth of items
|
||||
const cart = new ShoppingCart();
|
||||
cart.addItem({ price: 120, quantity: 1 });
|
||||
|
||||
// When: Getting the total
|
||||
const total = cart.getTotal();
|
||||
|
||||
// Then: 10% discount applied
|
||||
expect(total).toBe(108); // $120 - $12 (10%)
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**BDD Benefits**:
|
||||
- Tests readable by non-developers
|
||||
- Clear business requirements
|
||||
- Better stakeholder communication
|
||||
- Executable specifications
|
||||
|
||||
---
|
||||
|
||||
## TDD Patterns
|
||||
|
||||
### Pattern 1: Test List
|
||||
|
||||
Before coding, list all tests needed:
|
||||
|
||||
```markdown
|
||||
Calculator Tests:
|
||||
- [ ] add two positive numbers
|
||||
- [ ] add negative numbers
|
||||
- [ ] add zero
|
||||
- [ ] add multiple numbers
|
||||
- [ ] multiply two numbers
|
||||
- [ ] divide two numbers
|
||||
- [ ] divide by zero (error)
|
||||
```
|
||||
|
||||
Work through list one by one.
|
||||
|
||||
### Pattern 2: Fake It Till You Make It
|
||||
|
||||
Start with hardcoded returns, generalize later:
|
||||
|
||||
```typescript
|
||||
// Test 1: add(2, 3) = 5
|
||||
add(a, b) { return 5; } // Hardcoded!
|
||||
|
||||
// Test 2: add(5, 7) = 12
|
||||
add(a, b) { return a + b; } // Generalized
|
||||
```
|
||||
|
||||
### Pattern 3: Triangulation
|
||||
|
||||
Use multiple tests to force generalization:
|
||||
|
||||
```typescript
|
||||
// Test 1
|
||||
expect(fizzbuzz(3)).toBe('Fizz');
|
||||
|
||||
// Test 2
|
||||
expect(fizzbuzz(5)).toBe('Buzz');
|
||||
|
||||
// Test 3
|
||||
expect(fizzbuzz(15)).toBe('FizzBuzz');
|
||||
|
||||
// Forces complete implementation
|
||||
```
|
||||
|
||||
### Pattern 4: Test Data Builders
|
||||
|
||||
Create test helpers for complex objects:
|
||||
|
||||
```typescript
|
||||
class UserBuilder {
|
||||
private user = { name: 'Test', email: 'test@example.com', role: 'user' };
|
||||
|
||||
withName(name: string) {
|
||||
this.user.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
withRole(role: string) {
|
||||
this.user.role = role;
|
||||
return this;
|
||||
}
|
||||
|
||||
build() {
|
||||
return this.user;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const admin = new UserBuilder().withRole('admin').build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Refactoring with Confidence
|
||||
|
||||
**The TDD Safety Net**
|
||||
|
||||
### Refactoring Types
|
||||
|
||||
**1. Extract Method**:
|
||||
```typescript
|
||||
// Before
|
||||
function processOrder(order) {
|
||||
const total = order.items.reduce((sum, item) => sum + item.price, 0);
|
||||
const tax = total * 0.1;
|
||||
return total + tax;
|
||||
}
|
||||
|
||||
// After (refactored with test safety)
|
||||
function calculateTotal(items) {
|
||||
return items.reduce((sum, item) => sum + item.price, 0);
|
||||
}
|
||||
|
||||
function calculateTax(total) {
|
||||
return total * 0.1;
|
||||
}
|
||||
|
||||
function processOrder(order) {
|
||||
const total = calculateTotal(order.items);
|
||||
const tax = calculateTax(total);
|
||||
return total + tax;
|
||||
}
|
||||
```
|
||||
|
||||
**2. Remove Duplication**:
|
||||
```typescript
|
||||
// Tests force you to see duplication
|
||||
it('should validate email', () => {
|
||||
expect(validateEmail('test@example.com')).toBe(true);
|
||||
expect(validateEmail('invalid')).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate phone', () => {
|
||||
expect(validatePhone('+1-555-0100')).toBe(true);
|
||||
expect(validatePhone('invalid')).toBe(false);
|
||||
});
|
||||
|
||||
// Extract common validation pattern
|
||||
```
|
||||
|
||||
### Refactoring Workflow
|
||||
|
||||
```
|
||||
1. All tests GREEN? → Continue
|
||||
2. Identify code smell
|
||||
3. Make small refactoring
|
||||
4. Run tests → GREEN? → Continue
|
||||
5. Repeat until satisfied
|
||||
6. Commit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TDD Anti-Patterns
|
||||
|
||||
### ❌ Testing Implementation Details
|
||||
|
||||
```typescript
|
||||
// BAD: Testing private method
|
||||
it('should call _validateEmail internally', () => {
|
||||
spyOn(service, '_validateEmail');
|
||||
service.createUser({ email: 'test@example.com' });
|
||||
expect(service._validateEmail).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// GOOD: Testing behavior
|
||||
it('should reject invalid email', () => {
|
||||
expect(() => service.createUser({ email: 'invalid' }))
|
||||
.toThrow('Invalid email');
|
||||
});
|
||||
```
|
||||
|
||||
### ❌ Writing Tests After Code
|
||||
|
||||
```typescript
|
||||
// Wrong order!
|
||||
1. Write implementation
|
||||
2. Write tests
|
||||
|
||||
// Correct TDD:
|
||||
1. Write test (RED)
|
||||
2. Write implementation (GREEN)
|
||||
3. Refactor
|
||||
```
|
||||
|
||||
### ❌ Large Tests
|
||||
|
||||
```typescript
|
||||
// BAD: Testing multiple behaviors
|
||||
it('should handle user lifecycle', () => {
|
||||
const user = createUser();
|
||||
updateUser(user, { name: 'New Name' });
|
||||
deleteUser(user);
|
||||
// Too much in one test!
|
||||
});
|
||||
|
||||
// GOOD: One behavior per test
|
||||
it('should create user', () => {
|
||||
const user = createUser();
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
it('should update user name', () => {
|
||||
const user = createUser();
|
||||
updateUser(user, { name: 'New Name' });
|
||||
expect(user.name).toBe('New Name');
|
||||
});
|
||||
```
|
||||
|
||||
### ❌ Skipping Refactor Phase
|
||||
|
||||
```typescript
|
||||
// Don't skip refactoring!
|
||||
RED → GREEN → REFACTOR → RED → GREEN → REFACTOR
|
||||
↑________________↑
|
||||
Always refactor!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mock-Driven TDD
|
||||
|
||||
**When testing with external dependencies**
|
||||
|
||||
### Strategy 1: Dependency Injection
|
||||
|
||||
```typescript
|
||||
class UserService {
|
||||
constructor(private db: Database) {} // Inject dependency
|
||||
|
||||
async getUser(id: string) {
|
||||
return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
|
||||
}
|
||||
}
|
||||
|
||||
// Test with mock
|
||||
const mockDb = { query: vi.fn().mockResolvedValue({ id: '123' }) };
|
||||
const service = new UserService(mockDb);
|
||||
```
|
||||
|
||||
### Strategy 2: Interface-Based Mocking
|
||||
|
||||
```typescript
|
||||
interface EmailService {
|
||||
send(to: string, subject: string, body: string): Promise<void>;
|
||||
}
|
||||
|
||||
class MockEmailService implements EmailService {
|
||||
sent: any[] = [];
|
||||
|
||||
async send(to: string, subject: string, body: string) {
|
||||
this.sent.push({ to, subject, body });
|
||||
}
|
||||
}
|
||||
|
||||
// Test with mock
|
||||
const mockEmail = new MockEmailService();
|
||||
const service = new UserService(mockEmail);
|
||||
await service.registerUser({ email: 'test@example.com' });
|
||||
expect(mockEmail.sent).toHaveLength(1);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SOLID Principles Through TDD
|
||||
|
||||
**TDD naturally leads to SOLID design**
|
||||
|
||||
### Single Responsibility (SRP)
|
||||
Tests reveal when class does too much:
|
||||
```typescript
|
||||
// Many tests for one class? Split it!
|
||||
describe('UserManager', () => {
|
||||
// 20+ tests here → Too many responsibilities
|
||||
});
|
||||
|
||||
// Refactor to multiple classes
|
||||
describe('UserCreator', () => { /* 5 tests */ });
|
||||
describe('UserValidator', () => { /* 5 tests */ });
|
||||
describe('UserNotifier', () => { /* 5 tests */ });
|
||||
```
|
||||
|
||||
### Open/Closed (OCP)
|
||||
Tests enable extension without modification:
|
||||
```typescript
|
||||
// Testable, extensible design
|
||||
interface PaymentProcessor {
|
||||
process(amount: number): Promise<void>;
|
||||
}
|
||||
|
||||
class StripeProcessor implements PaymentProcessor { }
|
||||
class PayPalProcessor implements PaymentProcessor { }
|
||||
```
|
||||
|
||||
### Dependency Inversion (DIP)
|
||||
TDD requires dependency injection:
|
||||
```typescript
|
||||
// Testable: Depends on abstraction
|
||||
class OrderService {
|
||||
constructor(private payment: PaymentProcessor) {}
|
||||
}
|
||||
|
||||
// Easy to test with mocks
|
||||
const mockPayment = new MockPaymentProcessor();
|
||||
const service = new OrderService(mockPayment);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### TDD Workflow
|
||||
```
|
||||
1. Write test (RED) → Fails ✅
|
||||
2. Minimal code (GREEN) → Passes ✅
|
||||
3. Refactor → Still passes ✅
|
||||
4. Repeat
|
||||
```
|
||||
|
||||
### Test Smells
|
||||
- Test too long (>20 lines)
|
||||
- Multiple assertions (>3)
|
||||
- Testing implementation
|
||||
- Unclear test name
|
||||
- Slow tests (>100ms)
|
||||
- Flaky tests
|
||||
|
||||
### When to Use TDD
|
||||
✅ New features
|
||||
✅ Bug fixes (add test first)
|
||||
✅ Refactoring
|
||||
✅ Complex logic
|
||||
✅ Public APIs
|
||||
|
||||
❌ Throwaway prototypes
|
||||
❌ UI layout (use E2E instead)
|
||||
❌ Highly experimental code
|
||||
|
||||
---
|
||||
|
||||
**This skill is self-contained and works in ANY user project.**
|
||||
519
skills/unit-testing-expert/SKILL.md
Normal file
519
skills/unit-testing-expert/SKILL.md
Normal file
@@ -0,0 +1,519 @@
|
||||
---
|
||||
name: unit-testing-expert
|
||||
description: Comprehensive unit testing expertise covering Vitest, Jest, test-driven development (TDD), mocking strategies, test coverage, snapshot testing, test architecture, testing patterns, dependency injection, test doubles (mocks, stubs, spies, fakes), async testing, error handling tests, parametric testing, test organization, code coverage analysis, mutation testing, and production-grade unit testing best practices. Activates for unit testing, vitest, jest, test-driven development, TDD, red-green-refactor, mocking, stubbing, spying, test doubles, test coverage, snapshot testing, test architecture, dependency injection, async testing, test patterns, code coverage, mutation testing, test isolation, test fixtures, AAA pattern, given-when-then, test organization, testing best practices, vi.fn, vi.mock, vi.spyOn, describe, it, expect, beforeEach, afterEach.
|
||||
---
|
||||
|
||||
# Unit Testing Expert
|
||||
|
||||
**Self-contained unit testing expertise for Vitest/Jest in ANY user project.**
|
||||
|
||||
---
|
||||
|
||||
## Test-Driven Development (TDD)
|
||||
|
||||
**Red-Green-Refactor Cycle**:
|
||||
|
||||
```typescript
|
||||
// 1. RED: Write failing test
|
||||
describe('Calculator', () => {
|
||||
it('should add two numbers', () => {
|
||||
const calc = new Calculator();
|
||||
expect(calc.add(2, 3)).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
// 2. GREEN: Minimal implementation
|
||||
class Calculator {
|
||||
add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. REFACTOR: Improve code
|
||||
class Calculator {
|
||||
add(...numbers: number[]): number {
|
||||
return numbers.reduce((sum, n) => sum + n, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**TDD Benefits**:
|
||||
- Better design (testable code)
|
||||
- Living documentation
|
||||
- Faster debugging
|
||||
- Higher confidence
|
||||
|
||||
---
|
||||
|
||||
## Vitest/Jest Fundamentals
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { UserService } from './UserService';
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new UserService();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create user', () => {
|
||||
const user = service.create({ name: 'John', email: 'john@test.com' });
|
||||
|
||||
expect(user).toMatchObject({
|
||||
id: expect.any(String),
|
||||
name: 'John',
|
||||
email: 'john@test.com'
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw for invalid email', () => {
|
||||
expect(() => {
|
||||
service.create({ name: 'John', email: 'invalid' });
|
||||
}).toThrow('Invalid email');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Async Testing
|
||||
|
||||
```typescript
|
||||
it('should fetch user from API', async () => {
|
||||
const user = await api.fetchUser('user-123');
|
||||
|
||||
expect(user).toEqual({
|
||||
id: 'user-123',
|
||||
name: 'John Doe'
|
||||
});
|
||||
});
|
||||
|
||||
// Testing async errors
|
||||
it('should handle API errors', async () => {
|
||||
await expect(api.fetchUser('invalid')).rejects.toThrow('User not found');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mocking Strategies
|
||||
|
||||
### 1. Mock Functions
|
||||
|
||||
```typescript
|
||||
// Mock a function
|
||||
const mockFn = vi.fn();
|
||||
mockFn.mockReturnValue(42);
|
||||
expect(mockFn()).toBe(42);
|
||||
|
||||
// Mock with implementation
|
||||
const mockAdd = vi.fn((a, b) => a + b);
|
||||
expect(mockAdd(2, 3)).toBe(5);
|
||||
|
||||
// Verify calls
|
||||
expect(mockFn).toHaveBeenCalledTimes(1);
|
||||
expect(mockFn).toHaveBeenCalledWith(expected);
|
||||
```
|
||||
|
||||
### 2. Mock Modules
|
||||
|
||||
```typescript
|
||||
// Mock entire module
|
||||
vi.mock('./database', () => ({
|
||||
query: vi.fn().mockResolvedValue([{ id: 1, name: 'Test' }])
|
||||
}));
|
||||
|
||||
import { query } from './database';
|
||||
|
||||
it('should fetch users from database', async () => {
|
||||
const users = await query('SELECT * FROM users');
|
||||
expect(users).toHaveLength(1);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Spies
|
||||
|
||||
```typescript
|
||||
// Spy on existing method
|
||||
const spy = vi.spyOn(console, 'log');
|
||||
|
||||
myFunction();
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('Expected message');
|
||||
spy.mockRestore();
|
||||
```
|
||||
|
||||
### 4. Mock Dependencies
|
||||
|
||||
```typescript
|
||||
class UserService {
|
||||
constructor(private db: Database) {}
|
||||
|
||||
async getUser(id: string) {
|
||||
return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
|
||||
}
|
||||
}
|
||||
|
||||
// Test with mock
|
||||
const mockDb = {
|
||||
query: vi.fn().mockResolvedValue({ id: '123', name: 'John' })
|
||||
};
|
||||
|
||||
const service = new UserService(mockDb);
|
||||
const user = await service.getUser('123');
|
||||
|
||||
expect(mockDb.query).toHaveBeenCalledWith(
|
||||
'SELECT * FROM users WHERE id = ?',
|
||||
['123']
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Patterns
|
||||
|
||||
### AAA Pattern (Arrange-Act-Assert)
|
||||
|
||||
```typescript
|
||||
it('should calculate total price', () => {
|
||||
// Arrange
|
||||
const cart = new ShoppingCart();
|
||||
cart.addItem({ price: 10, quantity: 2 });
|
||||
cart.addItem({ price: 5, quantity: 3 });
|
||||
|
||||
// Act
|
||||
const total = cart.getTotal();
|
||||
|
||||
// Assert
|
||||
expect(total).toBe(35);
|
||||
});
|
||||
```
|
||||
|
||||
### Given-When-Then (BDD)
|
||||
|
||||
```typescript
|
||||
describe('Shopping Cart', () => {
|
||||
it('should apply discount when total exceeds $100', () => {
|
||||
// Given: A cart with items totaling $120
|
||||
const cart = new ShoppingCart();
|
||||
cart.addItem({ price: 120, quantity: 1 });
|
||||
|
||||
// When: Getting the total
|
||||
const total = cart.getTotal();
|
||||
|
||||
// Then: 10% discount applied
|
||||
expect(total).toBe(108); // $120 - $12 (10%)
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Parametric Testing
|
||||
|
||||
```typescript
|
||||
describe.each([
|
||||
[2, 3, 5],
|
||||
[10, 5, 15],
|
||||
[-1, 1, 0],
|
||||
[0, 0, 0]
|
||||
])('Calculator.add(%i, %i)', (a, b, expected) => {
|
||||
it(`should return ${expected}`, () => {
|
||||
const calc = new Calculator();
|
||||
expect(calc.add(a, b)).toBe(expected);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Doubles
|
||||
|
||||
### Mocks vs Stubs vs Spies vs Fakes
|
||||
|
||||
**Mock**: Verifies behavior (calls, arguments)
|
||||
```typescript
|
||||
const mock = vi.fn();
|
||||
mock('test');
|
||||
expect(mock).toHaveBeenCalledWith('test');
|
||||
```
|
||||
|
||||
**Stub**: Returns predefined values
|
||||
```typescript
|
||||
const stub = vi.fn().mockReturnValue(42);
|
||||
expect(stub()).toBe(42);
|
||||
```
|
||||
|
||||
**Spy**: Observes real function
|
||||
```typescript
|
||||
const spy = vi.spyOn(obj, 'method');
|
||||
obj.method();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
```
|
||||
|
||||
**Fake**: Working implementation (simplified)
|
||||
```typescript
|
||||
class FakeDatabase {
|
||||
private data = new Map();
|
||||
|
||||
async save(key, value) {
|
||||
this.data.set(key, value);
|
||||
}
|
||||
|
||||
async get(key) {
|
||||
return this.data.get(key);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Coverage Analysis
|
||||
|
||||
### Running Coverage
|
||||
|
||||
```bash
|
||||
# Vitest
|
||||
vitest --coverage
|
||||
|
||||
# Jest
|
||||
jest --coverage
|
||||
```
|
||||
|
||||
### Coverage Thresholds
|
||||
|
||||
```javascript
|
||||
// vitest.config.ts
|
||||
export default {
|
||||
test: {
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'html', 'lcov'],
|
||||
lines: 80,
|
||||
functions: 80,
|
||||
branches: 80,
|
||||
statements: 80
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Coverage Best Practices
|
||||
|
||||
**✅ DO**:
|
||||
- Aim for 80-90% coverage
|
||||
- Focus on business logic
|
||||
- Test edge cases
|
||||
- Test error paths
|
||||
|
||||
**❌ DON'T**:
|
||||
- Chase 100% coverage
|
||||
- Test getters/setters only
|
||||
- Test framework code
|
||||
- Write tests just for coverage
|
||||
|
||||
---
|
||||
|
||||
## Snapshot Testing
|
||||
|
||||
### When to Use Snapshots
|
||||
|
||||
**Good use cases**:
|
||||
- UI component output
|
||||
- API responses
|
||||
- Configuration objects
|
||||
- Error messages
|
||||
|
||||
```typescript
|
||||
it('should render user card', () => {
|
||||
const card = renderUserCard({ name: 'John', role: 'Admin' });
|
||||
expect(card).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// Update snapshots: vitest -u
|
||||
```
|
||||
|
||||
**Avoid snapshots for**:
|
||||
- Dates/timestamps
|
||||
- Random values
|
||||
- Large objects (prefer specific assertions)
|
||||
|
||||
---
|
||||
|
||||
## Test Organization
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── services/
|
||||
│ ├── UserService.ts
|
||||
│ └── UserService.test.ts ← Co-located
|
||||
tests/
|
||||
├── unit/
|
||||
│ └── utils.test.ts
|
||||
├── integration/
|
||||
│ └── api.test.ts
|
||||
└── fixtures/
|
||||
└── users.json
|
||||
```
|
||||
|
||||
### Test Naming
|
||||
|
||||
**✅ GOOD**:
|
||||
```typescript
|
||||
describe('UserService.create', () => {
|
||||
it('should create user with valid email', () => {});
|
||||
it('should throw error for invalid email', () => {});
|
||||
it('should generate unique ID', () => {});
|
||||
});
|
||||
```
|
||||
|
||||
**❌ BAD**:
|
||||
```typescript
|
||||
describe('UserService', () => {
|
||||
it('test1', () => {});
|
||||
it('should work', () => {});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling Tests
|
||||
|
||||
```typescript
|
||||
// Synchronous errors
|
||||
it('should throw for negative numbers', () => {
|
||||
expect(() => sqrt(-1)).toThrow('Cannot compute square root of negative');
|
||||
});
|
||||
|
||||
// Async errors
|
||||
it('should reject for invalid ID', async () => {
|
||||
await expect(fetchUser('invalid')).rejects.toThrow('Invalid ID');
|
||||
});
|
||||
|
||||
// Error types
|
||||
it('should throw TypeError', () => {
|
||||
expect(() => doSomething()).toThrow(TypeError);
|
||||
});
|
||||
|
||||
// Custom errors
|
||||
it('should throw ValidationError', () => {
|
||||
expect(() => validate()).toThrow(ValidationError);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Isolation
|
||||
|
||||
### Reset State Between Tests
|
||||
|
||||
```typescript
|
||||
let service: UserService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new UserService();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
```
|
||||
|
||||
### Avoid Test Interdependence
|
||||
|
||||
**❌ BAD**:
|
||||
```typescript
|
||||
let user;
|
||||
|
||||
it('should create user', () => {
|
||||
user = createUser(); // Shared state
|
||||
});
|
||||
|
||||
it('should update user', () => {
|
||||
updateUser(user); // Depends on previous test
|
||||
});
|
||||
```
|
||||
|
||||
**✅ GOOD**:
|
||||
```typescript
|
||||
it('should update user', () => {
|
||||
const user = createUser();
|
||||
updateUser(user);
|
||||
expect(user.updated).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
**✅ DO**:
|
||||
- Write tests before code (TDD)
|
||||
- Test behavior, not implementation
|
||||
- One assertion per test (when possible)
|
||||
- Clear test names (should...)
|
||||
- Mock external dependencies
|
||||
- Test edge cases and errors
|
||||
- Keep tests fast (<100ms each)
|
||||
- Use descriptive variable names
|
||||
- Clean up after tests
|
||||
|
||||
**❌ DON'T**:
|
||||
- Test private methods directly
|
||||
- Share state between tests
|
||||
- Use real databases/APIs
|
||||
- Test framework code
|
||||
- Write fragile tests (implementation-dependent)
|
||||
- Skip error cases
|
||||
- Use magic numbers
|
||||
- Leave commented-out tests
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Assertions
|
||||
```typescript
|
||||
expect(value).toBe(expected); // ===
|
||||
expect(value).toEqual(expected); // Deep equality
|
||||
expect(value).toBeTruthy(); // Boolean true
|
||||
expect(value).toBeFalsy(); // Boolean false
|
||||
expect(array).toHaveLength(3); // Array length
|
||||
expect(array).toContain(item); // Array includes
|
||||
expect(string).toMatch(/pattern/); // Regex match
|
||||
expect(fn).toThrow(Error); // Throws error
|
||||
expect(obj).toHaveProperty('key'); // Has property
|
||||
expect(value).toBeCloseTo(0.3, 5); // Float comparison
|
||||
```
|
||||
|
||||
### Lifecycle Hooks
|
||||
```typescript
|
||||
beforeAll(() => {}); // Once before all tests
|
||||
beforeEach(() => {}); // Before each test
|
||||
afterEach(() => {}); // After each test
|
||||
afterAll(() => {}); // Once after all tests
|
||||
```
|
||||
|
||||
### Mock Utilities
|
||||
```typescript
|
||||
vi.fn() // Create mock
|
||||
vi.fn().mockReturnValue(x) // Return value
|
||||
vi.fn().mockResolvedValue(x) // Async return
|
||||
vi.fn().mockRejectedValue(e) // Async error
|
||||
vi.mock('./module') // Mock module
|
||||
vi.spyOn(obj, 'method') // Spy on method
|
||||
vi.clearAllMocks() // Clear call history
|
||||
vi.resetAllMocks() // Reset + clear
|
||||
vi.restoreAllMocks() // Restore originals
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**This skill is self-contained and works in ANY user project with Vitest/Jest.**
|
||||
Reference in New Issue
Block a user