Component Testing Patterns and Best Practices — When to Use CT vs E2E

Component testing and E2E testing are not competitors — they are complementary layers that catch different categories of defects. The challenge is knowing when to write a component test versus an E2E test. Over-investing in either layer wastes effort: too many E2E tests are slow and fragile; too many component tests miss integration defects. This lesson provides the decision framework and patterns that production teams use to allocate testing effort across both layers effectively.

CT vs E2E — The Decision Framework

The decision comes down to one question: does this test verify a single component’s rendering and interaction, or does it verify a cross-component workflow that involves routing, API integration, or multiple pages?

// Decision framework: Component Test or E2E Test?

const DECISION_FRAMEWORK = {
  'Use Component Testing when': [
    'Testing prop variations — different data, states, edge cases',
    'Testing conditional rendering — show/hide based on props',
    'Testing user interactions within one component — click, type, select',
    'Testing event emission — verify callback props are called correctly',
    'Testing CSS/layout edge cases — overflow, long text, responsive breakpoints',
    'Testing form validation logic — required fields, format rules, error messages',
    'Testing loading/error/empty states — difficult to reproduce in E2E',
    'Testing accessibility — ARIA attributes, keyboard navigation within a component',
  ],
  'Use E2E Testing when': [
    'Testing multi-page user journeys — login, browse, checkout',
    'Testing API integration — real backend responses, data persistence',
    'Testing routing — navigation between pages, deep links, back button',
    'Testing authentication flows — login, logout, session expiry, OAuth',
    'Testing cross-component state — items added in shop appear in cart',
    'Testing third-party integrations — payment, email, analytics',
    'Testing deployment verification — smoke tests after release',
  ],
};


// ── Practical allocation for a typical web application ──

const ALLOCATION = {
  'Component Tests (~60% of UI tests)': {
    count: '100-200 tests',
    speed: '< 2 minutes total',
    covers: [
      'Every prop variation of shared components (buttons, forms, cards)',
      'All validation rules and error states',
      'Loading, empty, and error UI states',
      'Edge cases: long text, zero values, null fields, missing images',
      'Responsive breakpoints for key components',
    ],
  },
  'E2E Tests (~40% of UI tests)': {
    count: '30-50 tests',
    speed: '5-15 minutes total',
    covers: [
      'Complete checkout flow (browse → cart → pay → confirmation)',
      'User registration and login',
      'Search and filter with real results',
      'Admin CRUD operations',
      'Critical error recovery flows',
    ],
  },
};


// ── Component testing patterns catalogue ──

const CT_PATTERNS = [
  {
    pattern: 'Props Matrix Testing',
    description: 'Test every meaningful combination of props',
    example: `
      // Test all button variants
      ['primary', 'secondary', 'danger'].forEach((variant) => {
        ['small', 'medium', 'large'].forEach((size) => {
          it(\`renders \${variant} \${size} button\`, () => {
            cy.mount(
Note: The 60/40 split (60% component tests, 40% E2E tests) is a guideline, not a rule. The ideal ratio depends on your application’s architecture. A component-heavy UI library (design system) benefits from 80% CT and 20% E2E. A workflow-heavy enterprise app (many multi-page forms and approval chains) benefits from 40% CT and 60% E2E. Adjust based on where your application’s defects historically cluster: if most bugs are rendering issues, increase CT; if most bugs are integration issues, increase E2E.
Tip: The “props matrix” pattern — looping through every combination of variant, size, and state props — is the highest-ROI component testing pattern. A single test file with nested loops can generate 50+ test cases that verify every visual combination of a shared button, card, or badge component. These tests run in under 10 seconds total and catch CSS regressions that would take hours to verify manually across every combination.
Warning: Do not test implementation details in component tests. Checking that useState was called with a specific value, or that a specific CSS class was applied, makes tests brittle — they break when the implementation changes even if the visual output is identical. Test what the user sees and interacts with: visible text, element visibility, click responses, and emitted events. If a developer refactors the internal state management without changing the component’s external behaviour, the tests should still pass.

Common Mistakes

Mistake 1 — Writing E2E tests for component-level concerns

❌ Wrong: An E2E test that logs in, navigates to the product page, and checks that the “Out of Stock” badge renders correctly — 15 seconds to test one component’s conditional rendering.

✅ Correct: A component test: cy.mount(<ProductCard inStock={false} />) → verify badge renders. Takes 0.3 seconds, no login, no navigation, no backend.

Mistake 2 — Testing internal state instead of visible behaviour

❌ Wrong: Asserting that React’s useState returns a specific value or that an internal variable equals a certain number.

✅ Correct: Asserting on what the user sees: cy.get('[data-cy="counter"]').should('have.text', '5') — verifies the rendered output, not the internal mechanism.

🧠 Test Yourself

You need to verify that a Modal component closes when the user presses Escape. Should this be a component test or an E2E test?