Comprehensive guide to testing strategies for different application types and scenarios.
Provides comprehensive testing strategies for different application types, including Pyramid, Trophy, TDD, and BDD approaches. Use this guide to implement proper test distribution, write effective unit/integration/E2E tests, and apply best practices for API, frontend, and microservices testing.
/plugin marketplace add anton-abyzov/specweave/plugin install sw-testing@specweaveComprehensive guide to testing strategies for different application types and scenarios.
The Testing Pyramid emphasizes a broad base of fast, cheap unit tests, fewer integration tests, and minimal UI/E2E tests.
/\
/ \ E2E (10%)
/----\
/ \ Integration (20%)
/--------\
/ \ Unit (70%)
/--------------\
Unit Test (70% of suite):
// src/utils/cart.test.ts
import { describe, it, expect } from 'vitest';
import { calculateTotal, applyDiscount } from './cart';
describe('Cart Utils', () => {
describe('calculateTotal', () => {
it('should sum item prices', () => {
const items = [
{ id: 1, price: 10 },
{ id: 2, price: 20 },
];
expect(calculateTotal(items)).toBe(30);
});
it('should return 0 for empty cart', () => {
expect(calculateTotal([])).toBe(0);
});
it('should handle decimal prices', () => {
const items = [
{ id: 1, price: 10.99 },
{ id: 2, price: 20.50 },
];
expect(calculateTotal(items)).toBeCloseTo(31.49, 2);
});
});
describe('applyDiscount', () => {
it('should apply percentage discount', () => {
expect(applyDiscount(100, 'SAVE20', { type: 'percentage', value: 20 })).toBe(80);
});
it('should apply fixed discount', () => {
expect(applyDiscount(100, 'SAVE10', { type: 'fixed', value: 10 })).toBe(90);
});
it('should not go below zero', () => {
expect(applyDiscount(50, 'SAVE100', { type: 'fixed', value: 100 })).toBe(0);
});
});
});
Integration Test (20% of suite):
// src/api/orders.integration.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createTestServer } from '../test-utils/server';
import { seedDatabase, clearDatabase } from '../test-utils/database';
describe('Orders API Integration', () => {
let server: TestServer;
beforeEach(async () => {
server = await createTestServer();
await seedDatabase();
});
afterEach(async () => {
await clearDatabase();
await server.close();
});
it('should create order and store in database', async () => {
const response = await server.request
.post('/api/orders')
.send({
userId: 'user-123',
items: [{ productId: 'prod-1', quantity: 2 }],
});
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
id: expect.any(String),
userId: 'user-123',
status: 'pending',
});
// Verify database persistence
const order = await server.db.orders.findById(response.body.id);
expect(order).toBeTruthy();
expect(order.items).toHaveLength(1);
});
it('should send confirmation email on order creation', async () => {
const emailSpy = vi.spyOn(server.emailService, 'send');
await server.request
.post('/api/orders')
.send({
userId: 'user-123',
items: [{ productId: 'prod-1', quantity: 1 }],
});
expect(emailSpy).toHaveBeenCalledWith({
to: 'user@example.com',
subject: 'Order Confirmation',
template: 'order-confirmation',
});
});
});
E2E Test (10% of suite):
// e2e/checkout-flow.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Checkout Flow', () => {
test('should complete purchase as guest user', async ({ page }) => {
// Navigate to product
await page.goto('/products/laptop-123');
await expect(page.getByRole('heading', { name: 'Gaming Laptop' })).toBeVisible();
// Add to cart
await page.getByRole('button', { name: 'Add to Cart' }).click();
await expect(page.getByText('Item added to cart')).toBeVisible();
// Go to checkout
await page.getByRole('link', { name: 'Cart (1)' }).click();
await page.getByRole('button', { name: 'Checkout' }).click();
// Fill shipping info
await page.getByLabel('Email').fill('guest@example.com');
await page.getByLabel('Full Name').fill('John Doe');
await page.getByLabel('Address').fill('123 Main St');
await page.getByLabel('City').fill('New York');
await page.getByLabel('Zip Code').fill('10001');
// Fill payment info (test mode)
await page.getByLabel('Card Number').fill('4242424242424242');
await page.getByLabel('Expiry Date').fill('12/25');
await page.getByLabel('CVC').fill('123');
// Submit order
await page.getByRole('button', { name: 'Place Order' }).click();
// Verify confirmation
await expect(page).toHaveURL(/\/order-confirmation/);
await expect(page.getByText('Order Confirmed!')).toBeVisible();
await expect(page.getByText(/Order #/)).toBeVisible();
});
});
Modern approach that emphasizes integration tests over unit tests, with static analysis as the foundation.
/\
/ \ E2E (5%)
/----\
/ \ Integration (50%)
/--------\
/ \ Unit (25%)
/--------------\
/ \ Static (20%)
/------------------\
Static Analysis (20%):
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
// .eslintrc.json
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended"
],
"rules": {
"@typescript-eslint/no-explicit-any": "error",
"react-hooks/exhaustive-deps": "error"
}
}
Unit Tests (25%):
// src/utils/formatters.test.ts
import { describe, it, expect } from 'vitest';
import { formatCurrency, formatDate } from './formatters';
describe('Formatters (Pure Functions)', () => {
describe('formatCurrency', () => {
it('should format USD currency', () => {
expect(formatCurrency(1234.56, 'USD')).toBe('$1,234.56');
});
it('should handle negative amounts', () => {
expect(formatCurrency(-100, 'USD')).toBe('-$100.00');
});
});
describe('formatDate', () => {
it('should format ISO date', () => {
const date = new Date('2025-01-15');
expect(formatDate(date, 'short')).toBe('1/15/2025');
});
});
});
Integration Tests (50%):
// src/components/UserProfile.integration.test.tsx
import { describe, it, expect, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { UserProfile } from './UserProfile';
const server = setupServer(
rest.get('/api/users/:id', (req, res, ctx) => {
return res(
ctx.json({
id: req.params.id,
name: 'John Doe',
email: 'john@example.com',
})
);
}),
rest.put('/api/users/:id', (req, res, ctx) => {
return res(ctx.json({ ...req.body, updatedAt: Date.now() }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('UserProfile Integration', () => {
it('should load and display user data', async () => {
render(<UserProfile userId="123" />);
// Loading state
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Wait for data to load
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
it('should update user profile on form submit', async () => {
const user = userEvent.setup();
render(<UserProfile userId="123" />);
// Wait for initial load
await screen.findByText('John Doe');
// Edit name
const nameInput = screen.getByLabelText('Name');
await user.clear(nameInput);
await user.type(nameInput, 'Jane Smith');
// Submit form
await user.click(screen.getByRole('button', { name: 'Save' }));
// Verify success message
await waitFor(() => {
expect(screen.getByText('Profile updated successfully')).toBeInTheDocument();
});
// Verify updated name
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
});
it('should handle API errors gracefully', async () => {
// Mock API error
server.use(
rest.get('/api/users/:id', (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
})
);
render(<UserProfile userId="123" />);
// Wait for error message
await waitFor(() => {
expect(screen.getByText(/Failed to load user/i)).toBeInTheDocument();
});
});
});
E2E Tests (5%):
// e2e/critical-path.spec.ts
import { test, expect } from '@playwright/test';
test('critical user journey: signup to first purchase', async ({ page }) => {
// Only test the MOST critical path
await page.goto('/');
// Signup
await page.getByRole('link', { name: 'Sign Up' }).click();
await page.getByLabel('Email').fill('newuser@example.com');
await page.getByLabel('Password').fill('SecurePass123!');
await page.getByRole('button', { name: 'Create Account' }).click();
// Verify logged in
await expect(page.getByText('Welcome, New User')).toBeVisible();
// Make purchase
await page.goto('/products/best-seller');
await page.getByRole('button', { name: 'Buy Now' }).click();
await page.getByLabel('Card Number').fill('4242424242424242');
await page.getByRole('button', { name: 'Complete Purchase' }).click();
// Verify success
await expect(page.getByText('Purchase Successful')).toBeVisible();
});
RED: Write Failing Test:
// src/cart/ShoppingCart.test.ts
import { describe, it, expect } from 'vitest';
import { ShoppingCart } from './ShoppingCart';
describe('ShoppingCart', () => {
it('should add item to cart', () => {
const cart = new ShoppingCart();
cart.addItem({ id: 1, name: 'Laptop', price: 1000 });
expect(cart.getItemCount()).toBe(1);
});
});
Run test: ❌ FAIL (ShoppingCart doesn't exist)
GREEN: Minimal Implementation:
// src/cart/ShoppingCart.ts
interface CartItem {
id: number;
name: string;
price: number;
}
export class ShoppingCart {
private items: CartItem[] = [];
addItem(item: CartItem): void {
this.items.push(item);
}
getItemCount(): number {
return this.items.length;
}
}
Run test: ✅ PASS
Add Another Test (Triangulation):
it('should calculate total price', () => {
const cart = new ShoppingCart();
cart.addItem({ id: 1, name: 'Laptop', price: 1000 });
cart.addItem({ id: 2, name: 'Mouse', price: 50 });
expect(cart.getTotal()).toBe(1050);
});
Run test: ❌ FAIL (getTotal doesn't exist)
GREEN: Implement getTotal:
export class ShoppingCart {
// ... previous code ...
getTotal(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
Run test: ✅ PASS
REFACTOR: Improve Design:
export class ShoppingCart {
private items: Map<number, CartItem> = new Map();
addItem(item: CartItem): void {
const existing = this.items.get(item.id);
if (existing) {
// Increment quantity instead of duplicating
existing.quantity = (existing.quantity || 1) + 1;
} else {
this.items.set(item.id, { ...item, quantity: 1 });
}
}
getItemCount(): number {
return Array.from(this.items.values()).reduce(
(count, item) => count + (item.quantity || 1),
0
);
}
getTotal(): number {
return Array.from(this.items.values()).reduce(
(sum, item) => sum + item.price * (item.quantity || 1),
0
);
}
}
Run tests: ✅ ALL PASS (refactoring didn't break anything!)
Unit Test (Business Logic):
// src/services/OrderService.test.ts
import { describe, it, expect, vi } from 'vitest';
import { OrderService } from './OrderService';
describe('OrderService', () => {
it('should calculate order total with tax', () => {
const mockRepo = {
save: vi.fn(),
findById: vi.fn(),
};
const service = new OrderService(mockRepo);
const order = service.calculateTotal({
items: [{ price: 100, quantity: 2 }],
taxRate: 0.08,
});
expect(order.subtotal).toBe(200);
expect(order.tax).toBeCloseTo(16, 2);
expect(order.total).toBeCloseTo(216, 2);
});
});
Integration Test (API + Database):
// tests/integration/orders.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import supertest from 'supertest';
import { createTestApp } from '../utils/test-app';
describe('Orders API', () => {
let app;
let request;
beforeAll(async () => {
app = await createTestApp();
request = supertest(app);
});
afterAll(async () => {
await app.close();
});
it('POST /api/orders should create order', async () => {
const response = await request
.post('/api/orders')
.send({
userId: 'user-123',
items: [
{ productId: 'prod-1', quantity: 2, price: 50 },
],
})
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(String),
userId: 'user-123',
status: 'pending',
total: 100,
});
// Verify database persistence
const order = await app.db.orders.findById(response.body.id);
expect(order).toBeTruthy();
});
it('GET /api/orders/:id should return order', async () => {
// Create order first
const createResponse = await request
.post('/api/orders')
.send({ userId: 'user-123', items: [] });
const orderId = createResponse.body.id;
// Fetch order
const response = await request
.get(`/api/orders/${orderId}`)
.expect(200);
expect(response.body.id).toBe(orderId);
});
it('PUT /api/orders/:id/status should update status', async () => {
const createResponse = await request
.post('/api/orders')
.send({ userId: 'user-123', items: [] });
const orderId = createResponse.body.id;
const response = await request
.put(`/api/orders/${orderId}/status`)
.send({ status: 'shipped' })
.expect(200);
expect(response.body.status).toBe('shipped');
});
});
Contract Test (Pact):
// tests/contract/orders-consumer.test.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { OrdersClient } from '@/api/orders-client';
const provider = new PactV3({
consumer: 'OrdersConsumer',
provider: 'OrdersAPI',
});
describe('Orders API Contract', () => {
it('should get order by ID', async () => {
await provider
.given('order with ID 123 exists')
.uponReceiving('a request for order 123')
.withRequest({
method: 'GET',
path: '/api/orders/123',
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: MatchersV3.string('123'),
userId: MatchersV3.string('user-456'),
status: MatchersV3.regex('pending|shipped|delivered', 'pending'),
total: MatchersV3.decimal(100.50),
},
})
.executeTest(async (mockServer) => {
const client = new OrdersClient(mockServer.url);
const order = await client.getOrder('123');
expect(order.id).toBe('123');
expect(order.status).toMatch(/pending|shipped|delivered/);
});
});
});
[Document continues with 6 more comprehensive testing strategies...]
| Strategy | Best For | Coverage Target | Execution Time |
|---|---|---|---|
| Pyramid | Backend services, traditional apps | 80%+ unit, 100% critical | < 5 min |
| Trophy | Modern frontends (React, Vue) | 80%+ integration | < 3 min |
| TDD | New features, greenfield projects | 90%+ | Continuous |
| BDD | Stakeholder-driven development | 100% acceptance | Variable |
| API | REST/GraphQL services | 90%+ endpoints | < 2 min |
| Frontend | SPAs, component libraries | 85%+ components | < 4 min |
| Micro-Services | Distributed systems | 80%+ per service | < 10 min |
| Performance | High-traffic applications | Critical paths | 30 min |
| Security | Sensitive data, compliance | 100% attack vectors | 1 hour |
| Accessibility | Public-facing websites | WCAG AA 100% | 15 min |
Designs feature architectures by analyzing existing codebase patterns and conventions, then providing comprehensive implementation blueprints with specific files to create/modify, component designs, data flows, and build sequences