Testing Strategy
Testing Pyramid
Unit Tests
- Fast execution (< 1ms)
- Isolated components
- Business logic testing
- High coverage (80%+)
Integration Tests
- Medium execution (1-100ms)
- API endpoint testing
- Database integration
- External service mocking
E2E Tests
- Slower execution (100ms+)
- User workflow testing
- Cross-browser testing
- Critical path coverage
Testing Tools
Core Testing Framework
Vitest
Vitest
- Fast unit testing framework
- Jest-compatible API
- TypeScript support
- Watch mode and coverage
React Testing Library
React Testing Library
- Component testing utilities
- User-centric testing approach
- Accessibility testing
- Custom render functions
Playwright
Playwright
- E2E testing framework
- Cross-browser testing
- Mobile device testing
- Visual regression testing
Unit Testing
Component Testing
// tests/components/CustomerCard.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { CustomerCard } from '@/components/CustomerCard';
import { Customer } from '@/types/customer';
const mockCustomer: Customer = {
id: '1',
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
phone: '+1234567890',
createdAt: new Date('2024-01-01')
};
describe('CustomerCard', () => {
it('renders customer information correctly', () => {
render(<CustomerCard customer={mockCustomer} />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
expect(screen.getByText('+1234567890')).toBeInTheDocument();
});
it('calls onEdit when edit button is clicked', () => {
const onEdit = vi.fn();
render(<CustomerCard customer={mockCustomer} onEdit={onEdit} />);
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
expect(onEdit).toHaveBeenCalledWith(mockCustomer);
});
it('shows loading state when customer is being updated', () => {
render(<CustomerCard customer={mockCustomer} isLoading={true} />);
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
});
});
Business Logic Testing
// tests/utils/calculations.test.ts
import { calculateTotal, calculateTax, formatCurrency } from '@/utils/calculations';
describe('Business Logic Calculations', () => {
describe('calculateTotal', () => {
it('calculates total with tax correctly', () => {
const items = [
{ price: 100, quantity: 2 },
{ price: 50, quantity: 1 }
];
const taxRate = 0.1;
const result = calculateTotal(items, taxRate);
expect(result).toBe(275); // (200 + 50) * 1.1
});
it('handles empty items array', () => {
const result = calculateTotal([], 0.1);
expect(result).toBe(0);
});
it('handles zero tax rate', () => {
const items = [{ price: 100, quantity: 1 }];
const result = calculateTotal(items, 0);
expect(result).toBe(100);
});
});
describe('formatCurrency', () => {
it('formats USD correctly', () => {
expect(formatCurrency(1234.56, 'USD')).toBe('$1,234.56');
});
it('formats EUR correctly', () => {
expect(formatCurrency(1234.56, 'EUR')).toBe('€1,234.56');
});
it('handles zero amount', () => {
expect(formatCurrency(0, 'USD')).toBe('$0.00');
});
});
});
API Route Testing
// tests/api/customers.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { GET, POST } from '@/app/api/customers/route';
import { NextRequest } from 'next/server';
import { database } from '@/lib/database';
// Mock database
vi.mock('@/lib/database', () => ({
database: {
customer: {
findMany: vi.fn(),
create: vi.fn(),
findUnique: vi.fn()
}
}
}));
describe('/api/customers', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('GET', () => {
it('returns customers list', async () => {
const mockCustomers = [
{ id: '1', firstName: 'John', lastName: 'Doe', email: 'john@example.com' }
];
database.customer.findMany.mockResolvedValue(mockCustomers);
const request = new NextRequest('http://localhost:3000/api/customers');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.data).toEqual(mockCustomers);
});
it('handles search parameters', async () => {
const request = new NextRequest('http://localhost:3000/api/customers?search=john');
await GET(request);
expect(database.customer.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
OR: expect.arrayContaining([
{ firstName: { contains: 'john', mode: 'insensitive' } }
])
})
})
);
});
});
describe('POST', () => {
it('creates a new customer', async () => {
const customerData = {
firstName: 'Jane',
lastName: 'Smith',
email: 'jane@example.com'
};
const mockCustomer = { id: '2', ...customerData };
database.customer.create.mockResolvedValue(mockCustomer);
database.customer.findUnique.mockResolvedValue(null);
const request = new NextRequest('http://localhost:3000/api/customers', {
method: 'POST',
body: JSON.stringify(customerData),
headers: { 'Content-Type': 'application/json' }
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(201);
expect(data.data).toEqual(mockCustomer);
});
it('returns error for duplicate email', async () => {
const customerData = {
firstName: 'Jane',
lastName: 'Smith',
email: 'jane@example.com'
};
database.customer.findUnique.mockResolvedValue({ id: '1' });
const request = new NextRequest('http://localhost:3000/api/customers', {
method: 'POST',
body: JSON.stringify(customerData),
headers: { 'Content-Type': 'application/json' }
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(409);
expect(data.error).toContain('already exists');
});
});
});
Integration Testing
Database Integration
// tests/integration/database.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { PrismaClient } from '@prisma/client';
import { seedDatabase, cleanupDatabase } from '@/tests/helpers/database';
const prisma = new PrismaClient();
describe('Database Integration', () => {
beforeAll(async () => {
await seedDatabase(prisma);
});
afterAll(async () => {
await cleanupDatabase(prisma);
await prisma.$disconnect();
});
it('creates and retrieves customer', async () => {
const customerData = {
firstName: 'Integration',
lastName: 'Test',
email: 'integration@test.com',
teamId: 'test-team'
};
const customer = await prisma.customer.create({
data: customerData
});
expect(customer.id).toBeDefined();
expect(customer.firstName).toBe(customerData.firstName);
const retrieved = await prisma.customer.findUnique({
where: { id: customer.id }
});
expect(retrieved).toEqual(customer);
});
it('handles customer relationships', async () => {
const customer = await prisma.customer.create({
data: {
firstName: 'Customer',
lastName: 'WithOrders',
email: 'customer@test.com',
teamId: 'test-team'
}
});
const order = await prisma.order.create({
data: {
customerId: customer.id,
total: 100.00,
status: 'PENDING',
teamId: 'test-team'
}
});
const customerWithOrders = await prisma.customer.findUnique({
where: { id: customer.id },
include: { orders: true }
});
expect(customerWithOrders?.orders).toHaveLength(1);
expect(customerWithOrders?.orders[0].id).toBe(order.id);
});
});
API Integration
// tests/integration/api.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createTestServer } from '@/tests/helpers/server';
import { seedTestData } from '@/tests/helpers/seed';
describe('API Integration', () => {
let server: any;
beforeAll(async () => {
server = await createTestServer();
await seedTestData();
});
afterAll(async () => {
await server.close();
});
it('completes customer creation workflow', async () => {
// Create customer
const createResponse = await server.post('/api/customers')
.send({
firstName: 'API',
lastName: 'Test',
email: 'api@test.com'
})
.expect(201);
const customerId = createResponse.body.data.id;
// Create order for customer
const orderResponse = await server.post('/api/orders')
.send({
customerId,
items: [
{ productId: '1', quantity: 2, price: 50.00 }
],
total: 100.00
})
.expect(201);
// Verify customer has order
const customerResponse = await server.get(`/api/customers/${customerId}`)
.expect(200);
expect(customerResponse.body.data.orders).toHaveLength(1);
expect(customerResponse.body.data.orders[0].id).toBe(orderResponse.body.data.id);
});
it('handles payment processing workflow', async () => {
const customer = await server.post('/api/customers')
.send({
firstName: 'Payment',
lastName: 'Test',
email: 'payment@test.com'
})
.expect(201);
const order = await server.post('/api/orders')
.send({
customerId: customer.body.data.id,
items: [{ productId: '1', quantity: 1, price: 100.00 }],
total: 100.00
})
.expect(201);
const payment = await server.post('/api/payments')
.send({
orderId: order.body.data.id,
amount: 100.00,
method: 'CARD',
status: 'COMPLETED'
})
.expect(201);
expect(payment.body.data.status).toBe('COMPLETED');
});
});
End-to-End Testing
User Workflow Testing
// tests/e2e/customer-workflow.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Customer Management Workflow', () => {
test('complete customer lifecycle', async ({ page }) => {
// Navigate to customers page
await page.goto('/customers');
// Add new customer
await page.click('[data-testid="add-customer-button"]');
await page.fill('[data-testid="first-name-input"]', 'E2E');
await page.fill('[data-testid="last-name-input"]', 'Test');
await page.fill('[data-testid="email-input"]', 'e2e@test.com');
await page.click('[data-testid="save-customer-button"]');
// Verify customer appears in list
await expect(page.locator('[data-testid="customer-list"]')).toContainText('E2E Test');
// Click on customer to view details
await page.click('[data-testid="customer-row"]:has-text("E2E Test")');
// Verify customer details page
await expect(page.locator('[data-testid="customer-name"]')).toContainText('E2E Test');
await expect(page.locator('[data-testid="customer-email"]')).toContainText('e2e@test.com');
// Add order for customer
await page.click('[data-testid="add-order-button"]');
await page.fill('[data-testid="order-total"]', '150.00');
await page.selectOption('[data-testid="order-status"]', 'PENDING');
await page.click('[data-testid="save-order-button"]');
// Verify order appears in customer's orders
await expect(page.locator('[data-testid="orders-list"]')).toContainText('$150.00');
// Edit customer information
await page.click('[data-testid="edit-customer-button"]');
await page.fill('[data-testid="phone-input"]', '+1234567890');
await page.click('[data-testid="save-changes-button"]');
// Verify phone number was updated
await expect(page.locator('[data-testid="customer-phone"]')).toContainText('+1234567890');
});
test('search and filter customers', async ({ page }) => {
await page.goto('/customers');
// Search for specific customer
await page.fill('[data-testid="search-input"]', 'E2E');
await page.press('[data-testid="search-input"]', 'Enter');
// Verify search results
await expect(page.locator('[data-testid="customer-list"]')).toContainText('E2E Test');
// Filter by status
await page.selectOption('[data-testid="status-filter"]', 'ACTIVE');
// Verify filtered results
const customerRows = page.locator('[data-testid="customer-row"]');
await expect(customerRows).toHaveCount(1);
});
});
Cross-Browser Testing
// tests/e2e/cross-browser.spec.ts
import { test, expect, devices } from '@playwright/test';
test.describe('Cross-Browser Compatibility', () => {
test('works on desktop Chrome', async ({ page }) => {
await page.goto('/');
await expect(page.locator('[data-testid="main-navigation"]')).toBeVisible();
});
test('works on mobile Safari', async ({ page }) => {
await page.goto('/');
await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible();
});
test('responsive design works', async ({ page }) => {
// Test desktop view
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto('/customers');
await expect(page.locator('[data-testid="desktop-layout"]')).toBeVisible();
// Test mobile view
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/customers');
await expect(page.locator('[data-testid="mobile-layout"]')).toBeVisible();
});
});
Performance Testing
Load Testing
// tests/performance/load.test.ts
import { test, expect } from '@playwright/test';
test.describe('Performance Testing', () => {
test('handles concurrent customer creation', async ({ browser }) => {
const contexts = await Promise.all(
Array.from({ length: 10 }, () => browser.newContext())
);
const pages = await Promise.all(
contexts.map(context => context.newPage())
);
// Navigate all pages to customers
await Promise.all(
pages.map(page => page.goto('/customers'))
);
// Create customers concurrently
const customerPromises = pages.map((page, index) =>
page.click('[data-testid="add-customer-button"]')
.then(() => page.fill('[data-testid="first-name-input"]', `Load${index}`))
.then(() => page.fill('[data-testid="last-name-input"]', 'Test'))
.then(() => page.fill('[data-testid="email-input"]', `load${index}@test.com`))
.then(() => page.click('[data-testid="save-customer-button"]'))
);
await Promise.all(customerPromises);
// Verify all customers were created
const firstPage = pages[0];
await firstPage.reload();
const customerCount = await firstPage.locator('[data-testid="customer-row"]').count();
expect(customerCount).toBeGreaterThanOrEqual(10);
});
test('API response times are acceptable', async ({ request }) => {
const startTime = Date.now();
const response = await request.get('/api/customers');
const endTime = Date.now();
const responseTime = endTime - startTime;
expect(response.status()).toBe(200);
expect(responseTime).toBeLessThan(1000); // Should respond within 1 second
});
});
Test Configuration
Vitest Configuration
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import { resolve } from 'path';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'tests/',
'**/*.d.ts',
'**/*.config.*'
],
thresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
}
},
resolve: {
alias: {
'@': resolve(__dirname, './src')
}
}
});
Playwright Configuration
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
webServer: {
command: 'pnpm dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Test Utilities and Helpers
Database Helpers
// tests/helpers/database.ts
import { PrismaClient } from '@prisma/client';
export async function seedTestData(prisma: PrismaClient) {
await prisma.customer.createMany({
data: [
{
id: 'test-customer-1',
firstName: 'Test',
lastName: 'Customer',
email: 'test@example.com',
teamId: 'test-team'
}
]
});
}
export async function cleanupTestData(prisma: PrismaClient) {
await prisma.customer.deleteMany({
where: { teamId: 'test-team' }
});
}
Component Testing Helpers
// tests/helpers/render.tsx
import { render, RenderOptions } from '@testing-library/react';
import { ReactElement } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
{children}
</BrowserRouter>
</QueryClientProvider>
);
};
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>,
) => render(ui, { wrapper: AllTheProviders, ...options });
export * from '@testing-library/react';
export { customRender as render };
Best Practices
Testing Guidelines
- Write tests that test behavior, not implementation
- Use descriptive test names that explain what is being tested
- Keep tests simple and focused on one thing
- Use proper test data setup and teardown
- Mock external dependencies appropriately
Coverage Goals
- Aim for 80%+ code coverage
- Focus on critical business logic
- Test error conditions and edge cases
- Include accessibility testing
- Test both happy path and error scenarios
Continuous Integration
GitHub Actions
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Run unit tests
run: pnpm test:unit
- name: Run integration tests
run: pnpm test:integration
- name: Run E2E tests
run: pnpm test:e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
Next Steps
Monitoring & Observability
Set up comprehensive monitoring and observability for your application.
Performance Optimization
Learn about performance optimization techniques and best practices.
Security Testing
Implement security testing and vulnerability assessment.
API Documentation
Create comprehensive API documentation and testing.