๐Ÿงช Testing React + WordPress: Building Bulletproof Applications

Learn to write tests that catch bugs before your users do!

The Testing Pyramid: Your Quality Assurance Fortress

Testing is like building a fortress with multiple layers of defense. Each layer catches different types of problems, and together they create an impenetrable defense against bugs!

E2E Tests (Few) Integration (Some) Unit Tests (Many) The Testing Pyramid Fast & Isolated โ† โ†’ Slow & Realistic Cheap to Run โ† โ†’ Expensive to Maintain
graph LR A[Unit Tests] -->|70%| B[Test individual functions/components] C[Integration Tests] -->|20%| D[Test component interactions] E[E2E Tests] -->|10%| F[Test complete user flows] style A fill:#2ecc71 style C fill:#3498db style E fill:#e74c3c

Setting Up Your Testing Environment

Setting up testing is like preparing a laboratory - you need the right tools, a clean environment, and proper safety equipment (error handling)!

// package.json - Testing Dependencies
{
  "devDependencies": {
    // Testing Framework
    "@testing-library/react": "^14.0.0",
    "@testing-library/jest-dom": "^6.1.0",
    "@testing-library/user-event": "^14.5.0",
    
    // Test Runner
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    
    // Mocking & Utilities
    "msw": "^2.0.0",  // Mock Service Worker for API mocking
    "axios-mock-adapter": "^1.22.0",
    
    // E2E Testing
    "cypress": "^13.6.0",
    "@cypress/react": "^8.0.0",
    
    // Coverage & Reporting
    "jest-coverage-badges": "^1.0.0"
  },
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:e2e": "cypress open",
    "test:e2e:headless": "cypress run"
  }
}

Jest Configuration

// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['/src/setupTests.js'],
  moduleNameMapper: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '^@/(.*)$': '/src/$1'
  },
  collectCoverageFrom: [
    'src/**/*.{js,jsx}',
    '!src/index.js',
    '!src/serviceWorker.js'
  ],
  coverageThreshold: {
    global: {
      branches: 70,
      functions: 70,
      lines: 70,
      statements: 70
    }
  },
  testMatch: [
    '**/__tests__/**/*.[jt]s?(x)',
    '**/?(*.)+(spec|test).[jt]s?(x)'
  ]
};

Setup Test Utilities

// src/setupTests.js
import '@testing-library/jest-dom';
import { server } from './mocks/server';

// Establish API mocking before all tests
beforeAll(() => server.listen());

// Reset handlers after each test
afterEach(() => server.resetHandlers());

// Clean up after tests
afterAll(() => server.close());

// Custom matchers
expect.extend({
  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling;
    return {
      pass,
      message: () =>
        `expected ${received} to be within range ${floor} - ${ceiling}`
    };
  }
});

Unit Testing: Testing the LEGO Blocks

Unit tests are like testing individual LEGO blocks - making sure each piece works perfectly before building the castle!

Testing React Components

// Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

describe('Button Component', () => {
  test('renders with text', () => {
    render(<Button>Click me</Button>);
    
    const button = screen.getByRole('button', { name: /click me/i });
    expect(button).toBeInTheDocument();
  });

  test('handles click events', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click me</Button>);
    
    const button = screen.getByRole('button');
    fireEvent.click(button);
    
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  test('applies disabled state', () => {
    render(<Button disabled>Disabled</Button>);
    
    const button = screen.getByRole('button');
    expect(button).toBeDisabled();
    expect(button).toHaveClass('opacity-50');
  });

  test('shows loading spinner', () => {
    render(<Button loading>Loading</Button>);
    
    const spinner = screen.getByTestId('spinner');
    expect(spinner).toBeInTheDocument();
    expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true');
  });
});

Testing Custom Hooks

// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';

describe('useCounter Hook', () => {
  test('initializes with default value', () => {
    const { result } = renderHook(() => useCounter());
    
    expect(result.current.count).toBe(0);
  });

  test('initializes with custom value', () => {
    const { result } = renderHook(() => useCounter(10));
    
    expect(result.current.count).toBe(10);
  });

  test('increments counter', () => {
    const { result } = renderHook(() => useCounter());
    
    act(() => {
      result.current.increment();
    });
    
    expect(result.current.count).toBe(1);
  });

  test('decrements counter', () => {
    const { result } = renderHook(() => useCounter(5));
    
    act(() => {
      result.current.decrement();
    });
    
    expect(result.current.count).toBe(4);
  });

  test('resets to initial value', () => {
    const { result } = renderHook(() => useCounter(10));
    
    act(() => {
      result.current.increment();
      result.current.increment();
      result.current.reset();
    });
    
    expect(result.current.count).toBe(10);
  });
});

Integration Testing: Testing the Assembly

Integration tests are like testing a LEGO sub-assembly - making sure multiple pieces work together correctly!

graph TB A[Integration Test] --> B[Component A] A --> C[Component B] A --> D[API Mock] B --> E[State Management] C --> E E --> F[Rendered Output] D --> E style A fill:#3498db style D fill:#f39c12 style F fill:#2ecc71

Testing Component Integration

// BlogPost.integration.test.jsx
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import BlogPost from './BlogPost';
import { WordPressProvider } from './contexts/WordPressContext';

// Mock WordPress API
const server = setupServer(
  rest.get('/wp-json/wp/v2/posts/:id', (req, res, ctx) => {
    return res(
      ctx.json({
        id: req.params.id,
        title: { rendered: 'Test Post Title' },
        content: { rendered: '<p>Test content</p>' },
        author: 1,
        date: '2024-01-01T00:00:00'
      })
    );
  }),
  
  rest.get('/wp-json/wp/v2/comments', (req, res, ctx) => {
    return res(
      ctx.json([
        {
          id: 1,
          content: { rendered: 'Great post!' },
          author_name: 'John Doe',
          date: '2024-01-02T00:00:00'
        }
      ])
    );
  }),
  
  rest.post('/wp-json/wp/v2/comments', (req, res, ctx) => {
    return res(
      ctx.status(201),
      ctx.json({
        id: 2,
        content: { rendered: req.body.content },
        author_name: req.body.author_name
      })
    );
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('BlogPost Integration', () => {
  test('loads and displays post with comments', async () => {
    render(
      <WordPressProvider>
        <BlogPost postId="1" />
      </WordPressProvider>
    );
    
    // Wait for post to load
    await waitFor(() => {
      expect(screen.getByText('Test Post Title')).toBeInTheDocument();
    });
    
    // Check content
    expect(screen.getByText('Test content')).toBeInTheDocument();
    
    // Check comments loaded
    await waitFor(() => {
      expect(screen.getByText('Great post!')).toBeInTheDocument();
    });
    
    expect(screen.getByText('John Doe')).toBeInTheDocument();
  });

  test('submits new comment', async () => {
    const user = userEvent.setup();
    
    render(
      <WordPressProvider>
        <BlogPost postId="1" />
      </WordPressProvider>
    );
    
    // Wait for post to load
    await waitFor(() => {
      expect(screen.getByText('Test Post Title')).toBeInTheDocument();
    });
    
    // Fill comment form
    const nameInput = screen.getByLabelText(/your name/i);
    const commentInput = screen.getByLabelText(/your comment/i);
    const submitButton = screen.getByRole('button', { name: /submit/i });
    
    await user.type(nameInput, 'Jane Smith');
    await user.type(commentInput, 'Excellent article!');
    await user.click(submitButton);
    
    // Check comment was added
    await waitFor(() => {
      expect(screen.getByText('Excellent article!')).toBeInTheDocument();
    });
  });

  test('handles API errors gracefully', async () => {
    server.use(
      rest.get('/wp-json/wp/v2/posts/:id', (req, res, ctx) => {
        return res(ctx.status(500));
      })
    );
    
    render(
      <WordPressProvider>
        <BlogPost postId="1" />
      </WordPressProvider>
    );
    
    await waitFor(() => {
      expect(screen.getByText(/error loading post/i)).toBeInTheDocument();
    });
    
    // Check retry button exists
    const retryButton = screen.getByRole('button', { name: /retry/i });
    expect(retryButton).toBeInTheDocument();
  });
});

Testing WordPress API Integration

Testing API integration is like testing a phone line - you want to make sure messages get through correctly, even when the line is busy or disconnected!

API Service Testing

// WordPressAPI.test.js
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import WordPressAPI from './WordPressAPI';

describe('WordPress API Service', () => {
  let mock;
  
  beforeEach(() => {
    mock = new MockAdapter(axios);
  });
  
  afterEach(() => {
    mock.restore();
  });
  
  describe('getPosts', () => {
    test('fetches posts successfully', async () => {
      const mockPosts = [
        { id: 1, title: { rendered: 'Post 1' } },
        { id: 2, title: { rendered: 'Post 2' } }
      ];
      
      mock.onGet('/wp-json/wp/v2/posts').reply(200, mockPosts);
      
      const posts = await WordPressAPI.getPosts();
      
      expect(posts).toEqual(mockPosts);
      expect(mock.history.get[0].params).toEqual({ per_page: 10 });
    });
    
    test('handles pagination', async () => {
      mock.onGet('/wp-json/wp/v2/posts').reply(200, [], {
        'x-wp-total': '50',
        'x-wp-totalpages': '5'
      });
      
      const response = await WordPressAPI.getPosts({ page: 2 });
      
      expect(mock.history.get[0].params).toEqual({
        per_page: 10,
        page: 2
      });
    });
    
    test('handles network errors', async () => {
      mock.onGet('/wp-json/wp/v2/posts').networkError();
      
      await expect(WordPressAPI.getPosts()).rejects.toThrow('Network Error');
    });
    
    test('retries failed requests', async () => {
      let attempts = 0;
      mock.onGet('/wp-json/wp/v2/posts').reply(() => {
        attempts++;
        if (attempts < 3) {
          return [500, null];
        }
        return [200, [{ id: 1 }]];
      });
      
      const posts = await WordPressAPI.getPosts();
      
      expect(posts).toEqual([{ id: 1 }]);
      expect(attempts).toBe(3);
    });
  });
  
  describe('caching', () => {
    test('caches responses', async () => {
      const mockPosts = [{ id: 1 }];
      mock.onGet('/wp-json/wp/v2/posts').reply(200, mockPosts);
      
      // First call - hits API
      await WordPressAPI.getPosts();
      expect(mock.history.get).toHaveLength(1);
      
      // Second call - uses cache
      await WordPressAPI.getPosts();
      expect(mock.history.get).toHaveLength(1);
    });
    
    test('invalidates cache after TTL', async () => {
      jest.useFakeTimers();
      
      mock.onGet('/wp-json/wp/v2/posts').reply(200, []);
      
      await WordPressAPI.getPosts();
      expect(mock.history.get).toHaveLength(1);
      
      // Advance time past cache TTL
      jest.advanceTimersByTime(5 * 60 * 1000);
      
      await WordPressAPI.getPosts();
      expect(mock.history.get).toHaveLength(2);
      
      jest.useRealTimers();
    });
  });
});

End-to-End Testing: The Full Journey

E2E tests are like a test drive of your entire car - checking that everything works together from starting the engine to parking!

sequenceDiagram participant User participant Browser participant React App participant WordPress API User->>Browser: Navigate to site Browser->>React App: Load application React App->>WordPress API: Fetch initial data WordPress API-->>React App: Return posts React App-->>Browser: Render UI Browser-->>User: Display content User->>Browser: Click on post Browser->>React App: Route change React App->>WordPress API: Fetch post details WordPress API-->>React App: Return post data React App-->>Browser: Update view Browser-->>User: Show post Note over User,WordPress API: Complete user journey tested

Cypress E2E Tests

// cypress/e2e/blog-flow.cy.js
describe('Blog User Flow', () => {
  beforeEach(() => {
    // Seed test data
    cy.task('seedDatabase');
    cy.visit('/');
  });
  
  it('allows users to browse and comment on posts', () => {
    // Check homepage loads
    cy.contains('h1', 'My WordPress Blog').should('be.visible');
    
    // Verify posts are displayed
    cy.get('[data-testid="post-card"]').should('have.length.at.least', 3);
    
    // Search for a specific post
    cy.get('[data-testid="search-input"]').type('React Tutorial');
    cy.get('[data-testid="search-button"]').click();
    
    // Verify search results
    cy.get('[data-testid="post-card"]').should('have.length', 1);
    cy.contains('React Tutorial').should('be.visible');
    
    // Click on the post
    cy.get('[data-testid="post-card"]').first().click();
    
    // Verify post details page
    cy.url().should('include', '/posts/');
    cy.contains('h1', 'React Tutorial').should('be.visible');
    cy.get('[data-testid="post-content"]').should('be.visible');
    
    // Add a comment
    cy.get('[data-testid="comment-form"]').within(() => {
      cy.get('input[name="name"]').type('Test User');
      cy.get('input[name="email"]').type('test@example.com');
      cy.get('textarea[name="comment"]').type('Great tutorial!');
      cy.get('button[type="submit"]').click();
    });
    
    // Verify comment was added
    cy.get('[data-testid="comment"]').should('contain', 'Great tutorial!');
    cy.get('[data-testid="comment"]').should('contain', 'Test User');
    
    // Test pagination
    cy.visit('/');
    cy.get('[data-testid="pagination-next"]').click();
    cy.url().should('include', 'page=2');
    cy.get('[data-testid="post-card"]').should('exist');
  });
  
  it('handles errors gracefully', () => {
    // Simulate API failure
    cy.intercept('GET', '/wp-json/wp/v2/posts', {
      statusCode: 500,
      body: { message: 'Server error' }
    });
    
    cy.visit('/');
    
    // Check error message is displayed
    cy.contains('Unable to load posts').should('be.visible');
    cy.get('[data-testid="retry-button"]').should('be.visible');
    
    // Fix the API and retry
    cy.intercept('GET', '/wp-json/wp/v2/posts', {
      fixture: 'posts.json'
    });
    
    cy.get('[data-testid="retry-button"]').click();
    cy.get('[data-testid="post-card"]').should('exist');
  });
  
  it('is accessible', () => {
    cy.visit('/');
    cy.injectAxe();
    
    // Check for accessibility violations
    cy.checkA11y();
    
    // Navigate with keyboard
    cy.get('body').tab();
    cy.focused().should('have.attr', 'data-testid', 'skip-to-content');
    
    // Check ARIA labels
    cy.get('[role="navigation"]').should('have.attr', 'aria-label');
    cy.get('[role="main"]').should('exist');
    cy.get('[role="search"]').should('have.attr', 'aria-label');
  });
});

Test Coverage: Your Quality Report Card

Test coverage is like a report card for your code - it shows which parts are well-tested (A+) and which need more attention!

Coverage Report

Statements: 85%

85%

Branches: 78%

78%

Functions: 92%

92%

Lines: 88%

88%

Running Coverage Reports

// Generate coverage report
npm run test:coverage

// Coverage configuration in package.json
{
  "jest": {
    "collectCoverageFrom": [
      "src/**/*.{js,jsx}",
      "!src/index.js",
      "!src/serviceWorker.js",
      "!src/**/*.stories.js"
    ],
    "coverageReporters": [
      "text",
      "lcov",
      "html"
    ],
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    }
  }
}

Testing Best Practices

Testing best practices are like the rules of the road - they keep everyone safe and moving in the right direction!

The Golden Rules of Testing

โœ… DO

  • Write tests first (TDD)
  • Test behavior, not implementation
  • Keep tests simple and focused
  • Use descriptive test names
  • Mock external dependencies
  • Test edge cases

โŒ DON'T

  • Test implementation details
  • Write brittle tests
  • Ignore failing tests
  • Test third-party code
  • Over-mock everything
  • Write tests after bugs

Testing Checklist

// Testing Checklist for React + WordPress Apps

โ–ก Unit Tests
  โœ“ All utility functions tested
  โœ“ Component rendering tests
  โœ“ Event handler tests
  โœ“ Custom hooks tested
  โœ“ Edge cases covered

โ–ก Integration Tests
  โœ“ Component interactions tested
  โœ“ API integration tested
  โœ“ State management tested
  โœ“ Router integration tested
  โœ“ Error boundaries tested

โ–ก E2E Tests
  โœ“ Critical user paths tested
  โœ“ Form submissions work
  โœ“ Navigation works
  โœ“ Authentication flow tested
  โœ“ Error scenarios handled

โ–ก Performance Tests
  โœ“ Load time benchmarks
  โœ“ Bundle size checks
  โœ“ Memory leak detection
  โœ“ API response time tests

โ–ก Accessibility Tests
  โœ“ Keyboard navigation
  โœ“ Screen reader compatibility
  โœ“ Color contrast checks
  โœ“ ARIA labels present

โ–ก Security Tests
  โœ“ XSS prevention tested
  โœ“ Input validation tested
  โœ“ Authentication tested
  โœ“ Authorization tested

Continuous Integration: Automated Testing Pipeline

CI/CD is like having a robot assistant that runs all your tests every time you make changes - catching bugs before they reach production!

graph LR A[Push Code] --> B[GitHub Actions] B --> C[Install Dependencies] C --> D[Run Linting] D --> E[Run Unit Tests] E --> F[Run Integration Tests] F --> G[Build Application] G --> H[Run E2E Tests] H --> I{All Tests Pass?} I -->|Yes| J[Deploy to Staging] I -->|No| K[Notify Developer] style I fill:#f39c12 style J fill:#2ecc71 style K fill:#e74c3c

GitHub Actions Configuration

# .github/workflows/test.yml
name: Test Suite

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [18.x, 20.x]
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run linting
        run: npm run lint
      
      - name: Run unit tests
        run: npm test -- --coverage
      
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info
      
      - name: Build application
        run: npm run build
      
      - name: Run E2E tests
        uses: cypress-io/github-action@v5
        with:
          start: npm start
          wait-on: 'http://localhost:3000'
      
      - name: Upload test artifacts
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: |
            coverage/
            cypress/screenshots/
            cypress/videos/

Real-World Testing Scenario

Let's see how all these testing strategies come together in a real feature: a WordPress comment system with React!

โœ… Complete Test Suite Example

// Complete test suite for Comment System

// 1. Unit Test - Comment validation
describe('Comment Validation', () => {
  test('validates required fields', () => {
    const result = validateComment({
      author: '',
      content: 'Great post!'
    });
    
    expect(result.isValid).toBe(false);
    expect(result.errors).toContain('Author is required');
  });
});

// 2. Component Test - Comment Form
describe('CommentForm', () => {
  test('submits valid comment', async () => {
    const onSubmit = jest.fn();
    const { user } = render(<CommentForm onSubmit={onSubmit} />);
    
    await user.type(screen.getByLabelText(/name/i), 'John');
    await user.type(screen.getByLabelText(/comment/i), 'Nice!');
    await user.click(screen.getByRole('button', { name: /submit/i }));
    
    expect(onSubmit).toHaveBeenCalledWith({
      author: 'John',
      content: 'Nice!'
    });
  });
});

// 3. Integration Test - Comments with API
describe('Comments Integration', () => {
  test('loads and displays comments', async () => {
    render(<Comments postId="1" />);
    
    await waitFor(() => {
      expect(screen.getByText('First comment')).toBeInTheDocument();
    });
  });
});

// 4. E2E Test - Complete flow
describe('Comment User Flow', () => {
  it('user can add and view comment', () => {
    cy.visit('/posts/1');
    cy.get('[data-testid="comment-input"]').type('Awesome!');
    cy.get('[data-testid="submit-comment"]').click();
    cy.contains('Awesome!').should('be.visible');
  });
});