Front-end Engineering Lab
PatternsTesting Strategies

Testing Strategies

Comprehensive testing strategies for building reliable, maintainable frontend applications

Testing Strategies

Testing is easy. Testing well is hard. This section covers advanced testing strategies that go beyond basic unit tests to build truly reliable applications.

Why Advanced Testing Strategies Matter

Common Testing Mistakes:

  • Only testing happy paths
  • Ignoring edge cases
  • No visual regression testing
  • Skipping accessibility tests
  • Poor test maintainability
  • Slow, flaky tests

What This Section Covers: Advanced techniques that professional teams use to ship reliable software with confidence.

Testing Pyramid (Modern)

           ┌─────────────┐
           │   E2E Tests │  (Few, critical flows)
           │   5-10%     │
           ├─────────────┤
           │ Integration │  (Feature-level)
           │   20-30%    │
           ├─────────────┤
           │   Unit      │  (Functions, logic)
           │   60-70%    │
           └─────────────┘

But also include:

  • Visual regression tests
  • Accessibility tests
  • Performance tests
  • Contract tests
  • Mutation tests

Testing Philosophy

1. Test Behavior, Not Implementation

// ❌ BAD: Testing implementation details
test('uses useState hook', () => {
  const wrapper = shallow(<Counter />);
  expect(wrapper.find('useState')).toBeDefined();
});

// ✅ GOOD: Testing behavior
test('increments counter when button clicked', () => {
  render(<Counter />);
  
  const button = screen.getByRole('button', { name: /increment/i });
  fireEvent.click(button);
  
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

2. Write Tests Users Would Write

// ❌ BAD: Testing like a developer
test('input value updates state', () => {
  const { container } = render(<SearchBox />);
  const input = container.querySelector('[data-testid="search-input"]');
  // ...
});

// ✅ GOOD: Testing like a user
test('shows search results when user types', async () => {
  render(<SearchBox />);
  
  const user = userEvent.setup();
  const searchBox = screen.getByRole('searchbox');
  
  await user.type(searchBox, 'react');
  
  expect(await screen.findByText('React Documentation')).toBeVisible();
});

3. Test at the Right Level

// Unit test: Pure function
test('calculates total price correctly', () => {
  expect(calculateTotal([10, 20, 30])).toBe(60);
});

// Integration test: Feature
test('checkout flow calculates shipping and tax', () => {
  render(<CheckoutFlow items={mockItems} />);
  // Test the full feature
});

// E2E test: Critical path
test('user can complete purchase', () => {
  // Test end-to-end flow including API calls
});

Testing Coverage Strategy

TypeCoveragePurposeSpeed
Unit60-70%Business logic, utils⚡ Very Fast
Integration20-30%Features, components🚀 Fast
E2E5-10%Critical user flows🐌 Slow
VisualKey pagesUI consistency⚡ Fast
A11yAll pagesAccessibility⚡ Fast
PerformanceKey flowsSpeed metrics🐌 Slow
ContractAPI boundariesAPI compatibility🚀 Fast

What to Test

Must Test ✅

  • Critical user flows (signup, checkout, auth)
  • Business logic
  • Error handling
  • Accessibility
  • Edge cases
  • Security boundaries

Should Test ⚠️

  • Component interactions
  • Form validation
  • Navigation
  • State management
  • API integration
  • Loading states

Don't Test ❌

  • Third-party libraries
  • Implementation details
  • Trivial code
  • Framework internals
  • CSS styling (use visual regression instead)

Test Organization

tests/
├── unit/
│   ├── utils/
│   │   ├── formatPrice.test.ts
│   │   └── validation.test.ts
│   └── hooks/
│       └── useAuth.test.ts

├── integration/
│   ├── features/
│   │   ├── Checkout.test.tsx
│   │   └── Search.test.tsx
│   └── components/
│       └── ProductCard.test.tsx

├── e2e/
│   ├── auth/
│   │   ├── login.spec.ts
│   │   └── signup.spec.ts
│   └── checkout/
│       └── complete-purchase.spec.ts

├── visual/
│   └── Homepage.visual.ts

├── a11y/
│   └── Dashboard.a11y.test.ts

└── performance/
    └── product-list.perf.test.ts

Testing Tools Ecosystem

Testing Frameworks

  • Jest: Unit and integration tests
  • Vitest: Faster Jest alternative
  • Playwright: E2E testing
  • Cypress: Alternative E2E

Component Testing

  • React Testing Library: Component behavior
  • Testing Library: Framework-agnostic

Visual Testing

  • Percy: Visual regression
  • Chromatic: Storybook visual testing
  • BackstopJS: Self-hosted

Accessibility

  • axe-core: A11y testing
  • Pa11y: Automated accessibility
  • jest-axe: Jest integration

Performance

  • Lighthouse: Performance audits
  • k6: Load testing
  • Artillery: API load testing

Contract Testing

  • Pact: Consumer-driven contracts
  • MSW: API mocking

Mutation Testing

  • Stryker: Test quality measurement

Testing Best Practices

1. Arrange-Act-Assert (AAA)

test('adds item to cart', () => {
  // Arrange: Setup
  render(<Cart />);
  const addButton = screen.getByRole('button', { name: /add to cart/i });
  
  // Act: Perform action
  fireEvent.click(addButton);
  
  // Assert: Verify result
  expect(screen.getByText('1 item in cart')).toBeInTheDocument();
});

2. Test Isolation

// ❌ BAD: Tests depend on each other
test('creates user', () => {
  globalUser = createUser();
});

test('updates user', () => {
  updateUser(globalUser); // Depends on previous test
});

// ✅ GOOD: Each test is independent
test('creates user', () => {
  const user = createUser();
  expect(user).toBeDefined();
});

test('updates user', () => {
  const user = createUser(); // Create fresh user
  updateUser(user);
  expect(user.updated).toBe(true);
});

3. Descriptive Test Names

// ❌ BAD
test('test 1', () => {});
test('works', () => {});

// ✅ GOOD
test('displays error message when email is invalid', () => {});
test('disables submit button while form is submitting', () => {});
test('redirects to login page when user is not authenticated', () => {});

4. Use Test Helpers

// test-utils.tsx
import { render } from '@testing-library/react';
import { ThemeProvider } from '@/components/ThemeProvider';
import { AuthProvider } from '@/contexts/AuthContext';

export function renderWithProviders(ui: React.ReactElement, options = {}) {
  return render(
    <ThemeProvider>
      <AuthProvider>
        {ui}
      </AuthProvider>
    </ThemeProvider>,
    options
  );
}

// Usage in tests
test('displays user profile', () => {
  renderWithProviders(<UserProfile />);
  // ...
});

5. Mock External Dependencies

// Mock fetch API
global.fetch = jest.fn(() =>
  Promise.resolve({
    ok: true,
    json: async () => ({ data: 'mock data' }),
  })
) as jest.Mock;

// Mock router
jest.mock('next/navigation', () => ({
  useRouter: () => ({
    push: jest.fn(),
    pathname: '/test',
  }),
}));

Common Testing Patterns

Testing Async Operations

test('loads and displays data', async () => {
  render(<DataComponent />);
  
  // Wait for loading to finish
  await waitFor(() => {
    expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
  });
  
  // Check data is displayed
  expect(screen.getByText('Data loaded')).toBeInTheDocument();
});

Testing User Interactions

test('submits form with valid data', async () => {
  const user = userEvent.setup();
  const onSubmit = jest.fn();
  
  render(<Form onSubmit={onSubmit} />);
  
  await user.type(screen.getByLabelText('Email'), 'test@example.com');
  await user.type(screen.getByLabelText('Password'), 'password123');
  await user.click(screen.getByRole('button', { name: /submit/i }));
  
  expect(onSubmit).toHaveBeenCalledWith({
    email: 'test@example.com',
    password: 'password123',
  });
});

Testing Error States

test('displays error when API call fails', async () => {
  // Mock failed API call
  server.use(
    rest.get('/api/data', (req, res, ctx) => {
      return res(ctx.status(500), ctx.json({ error: 'Server error' }));
    })
  );
  
  render(<DataComponent />);
  
  expect(await screen.findByText('Failed to load data')).toBeInTheDocument();
});

CI/CD Integration

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: '20'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run unit tests
        run: npm run test:unit
      
      - name: Run integration tests
        run: npm run test:integration
      
      - name: Run E2E tests
        run: npm run test:e2e
      
      - name: Run accessibility tests
        run: npm run test:a11y
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3

Next Steps

Explore specific testing strategies:

Remember: Testing is easy. Testing well is hard. Master these strategies to build confidence in your code.

On this page