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
Copy
// 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: '[email protected]',
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('[email protected]')).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
Copy
// 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
Copy
// 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: '[email protected]' }
];
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: '[email protected]'
};
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: '[email protected]'
};
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
Copy
// 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: '[email protected]',
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: '[email protected]',
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
Copy
// 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: '[email protected]'
})
.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: '[email protected]'
})
.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
Copy
// 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"]', '[email protected]');
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('[email protected]');
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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: '[email protected]',
teamId: 'test-team'
}
]
});
}
export async function cleanupTestData(prisma: PrismaClient) {
await prisma.customer.deleteMany({
where: { teamId: 'test-team' }
});
}
Component Testing Helpers
Copy
// 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
Copy
# .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.