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!
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!
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!
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%
Branches: 78%
Functions: 92%
Lines: 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!
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');
});
});