Testing Strategies for Web Apps
Design a practical testing strategy covering unit tests, integration tests, and end-to-end tests for modern web applications.
TestingIntermediate18 min read
AI Summary
Quick context for Testing Strategies for Web Apps
Testing Strategies for Web Apps is a intermediate guide in Testing. Design a practical testing strategy covering unit tests, integration tests, and end-to-end tests for modern web applications.. Key topics: e2e, integration-tests, testing, unit-tests.
## The Testing Pyramid
The testing pyramid is a mental model for balancing test types. At the base are many fast, isolated unit tests. In the middle are fewer integration tests. At the top are a small number of end-to-end tests.
```
/ E2E \ (Few, slow, high confidence)
/----------\
/ Integration \ (Some, moderate speed)
/----------------\
/ Unit Tests \ (Many, fast, low cost)
/____________________\
```
Each layer serves a different purpose. No single layer replaces another.
## Unit Tests
Unit tests verify individual functions and components in isolation. They are fast, cheap to write, and pinpoint failures precisely.
```typescript
// Pure function — easy to unit test
function calculateDiscount(price: number, percentage: number): number {
if (percentage < 0 || percentage > 100) {
throw new Error('Invalid percentage');
}
return price * (1 - percentage / 100);
}
// Test file: calculateDiscount.test.ts
describe('calculateDiscount', () => {
it('applies percentage discount correctly', () => {
expect(calculateDiscount(100, 20)).toBe(80);
});
it('returns original price for 0% discount', () => {
expect(calculateDiscount(100, 0)).toBe(100);
});
it('throws for invalid percentage', () => {
expect(() => calculateDiscount(100, -5)).toThrow('Invalid percentage');
expect(() => calculateDiscount(100, 110)).toThrow('Invalid percentage');
});
});
```
Unit tests are ideal for business logic, utility functions, data transformations, and pure calculations.
## Integration Tests
Integration tests verify that multiple units work together correctly. They test API endpoints, database queries, and component interactions.
```typescript
// API integration test
describe('POST /api/orders', () => {
it('creates an order and deducts inventory', async () => {
const resp request(app)
.post('/api/orders')
.send({ productId: 'abc', quantity: 2 })
.expect(201);
expect(response.body.id).toBeDefined();
expect(response.body.total).toBeGreaterThan(0);
// Verify side effects
const inventory = await db.query(
'SELECT stock FROM products WHERE id = $1',
['abc']
);
expect(inventory.rows[0].stock).toBe(8); // Was 10
});
});
```
Integration tests catch issues that unit tests miss: incorrect SQL, middleware ordering, authentication bypass, and data serialization problems.
## End-to-End Tests
E2E tests simulate real user workflows in a browser. They are the slowest and most expensive tests but provide the highest confidence.
```typescript
// Playwright E2E test
test('user can add item to cart and checkout', async ({ page }) => {
await page.goto('/products');
// Add product to cart
await page.click('[data-testid="add-to-cart-button"]');
await expect(page.locator('[data-testid="cart-count"]')).toHaveText('1');
// Go to cart
await page.click('[data-testid="cart-link"]');
// Verify product is in cart
await expect(page.locator('.cart-item')).toHaveCount(1);
// Proceed to checkout
await page.click('[data-testid="checkout-button"]');
await page.fill('[name="email"]', 'test@example.com');
await page.click('[data-testid="place-order"]');
// Verify order confirmation
await expect(page.locator('h1')).toHaveText('Order Confirmed');
});
```
Limit E2E tests to critical user journeys: sign up, sign in, checkout, and core business workflows.
## What to Test
### Test Behavior, Not Implementation
```typescript
// Bad: testing implementation details
expect(component.state.isOpen).toBe(true);
expect(component.instance().handleClick).toHaveBeenCalled();
// Good: testing observable behavior
expect(screen.getByText('Menu')).toBeVisible();
await userEvent.click(screen.getByText('Menu'));
expect(screen.getByText('Settings')).toBeVisible();
```
### Test Edge Cases
- Empty states
- Error states
- Loading states
- Long text and overflow
- Network failures
- Unauthorized access attempts
### Test Accessibility
```typescript
test('checkout form is accessible', async ({ page }) => {
await page.goto('/checkout');
// Check form labels
const emailInput = page.locator('[name="email"]');
await expect(emailInput).toHaveAccessibleName('Email address');
// Check focus management
await page.keyboard.press('Tab');
expect(await page.evaluate(() => document.activeElement.tagName)).toBe('INPUT');
});
```
## Test Organization
Organize tests by feature, not by file type.
```
src/
features/
auth/
login.tsx
login.test.tsx
register.tsx
register.test.tsx
auth.service.ts
auth.service.test.ts
checkout/
checkout.tsx
checkout.test.tsx
checkout.service.ts
checkout.service.test.ts
```
## CI Integration
```yaml
# Example CI pipeline stages
stages:
- unit # Fast feedback, run on every commit
- integration # Verify contracts, run on every commit
- e2e # Critical paths, run on PRs and main
- accessibility # Catch regressions, run weekly
```
Run unit and integration tests on every commit. Run E2E tests on pull requests and before deployment. Run accessibility audits on a schedule.
## Coverage Targets
Aim for meaningful coverage, not 100%. Coverage measures code paths, not quality.
- **Unit tests**: 80-90% for business logic
- **Integration tests**: cover all API endpoints and data flows
- **E2E tests**: cover critical user journeys
```bash
# Check coverage
npx vitest run --coverage
# or
npx jest --coverage
```
## Common Mistakes
- **Testing too much at one layer.** Keep unit tests focused on logic, integration tests on boundaries, and E2E on workflows.
- **Testing framework internals.** Test what your code does, not how React or Express works.
- **Ignoring flaky tests.** A flaky test is worse than no test. Fix or remove it immediately.
- **Writing tests after the bug is fixed.** Write the failing test first to prove the bug exists, then fix it.
A good testing strategy lets you deploy with confidence. Start with the unit test layer, add integration tests for your API, and use E2E tests for your most critical paths.
Continue with related content
Suggested next reads and tools from the knowledge graph.