Skip to content

Latest commit

 

History

History
432 lines (347 loc) · 10.6 KB

File metadata and controls

432 lines (347 loc) · 10.6 KB

Testing 🧪

Testing frameworks and tools for ensuring code quality in React applications.

Jest

Description: Delightful JavaScript testing framework with a focus on simplicity and developer experience.

Key Features:

  • Zero configuration
  • Snapshot testing
  • Code coverage
  • Mocking capabilities
  • Parallel test execution
  • Built-in assertions
  • Watch mode

Installation:

npm install --save-dev jest
# or with React
npm install --save-dev jest @testing-library/jest-dom

Configuration (jest.config.js):

module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts'
  ]
};

Basic Usage:

// utils.test.js
import { add, multiply, formatCurrency } from './utils';

describe('Math utilities', () => {
  test('adds two numbers correctly', () => {
    expect(add(2, 3)).toBe(5);
    expect(add(-1, 1)).toBe(0);
  });

  test('multiplies two numbers correctly', () => {
    expect(multiply(2, 3)).toBe(6);
    expect(multiply(-2, 3)).toBe(-6);
  });
});

describe('Format utilities', () => {
  test('formats currency correctly', () => {
    expect(formatCurrency(123.45)).toBe('$123.45');
    expect(formatCurrency(0)).toBe('$0.00');
  });
});

// Async testing
describe('API utilities', () => {
  test('fetches user data', async () => {
    const user = await fetchUser(1);
    expect(user).toHaveProperty('id', 1);
    expect(user).toHaveProperty('name');
  });

  test('handles fetch errors', async () => {
    await expect(fetchUser(999)).rejects.toThrow('User not found');
  });
});

// Mocking
jest.mock('./api', () => ({
  fetchUser: jest.fn()
}));

import { fetchUser } from './api';

describe('With mocks', () => {
  test('uses mocked fetchUser', async () => {
    fetchUser.mockResolvedValue({ id: 1, name: 'John' });
    
    const user = await fetchUser(1);
    expect(fetchUser).toHaveBeenCalledWith(1);
    expect(user.name).toBe('John');
  });
});

Use Cases: Unit testing, integration testing, utility function testing

React Testing Library

Description: Simple and complete testing utilities for React components that encourage good testing practices.

Key Features:

  • User-centric testing
  • Accessibility testing
  • Component testing utilities
  • Custom render functions
  • Query utilities
  • Async utilities
  • Jest integration

Installation:

npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event

Setup (setupTests.js):

import '@testing-library/jest-dom';

Basic Usage:

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from './App';

describe('App Component', () => {
  test('renders welcome message', () => {
    render(<App />);
    expect(screen.getByText(/welcome/i)).toBeInTheDocument();
  });

  test('handles button click', async () => {
    const user = userEvent.setup();
    render(<App />);
    
    const button = screen.getByRole('button', { name: /click me/i });
    await user.click(button);
    
    expect(screen.getByText(/button clicked/i)).toBeInTheDocument();
  });

  test('handles form submission', async () => {
    const user = userEvent.setup();
    render(<App />);
    
    const input = screen.getByLabelText(/name/i);
    const submitButton = screen.getByRole('button', { name: /submit/i });
    
    await user.type(input, 'John Doe');
    await user.click(submitButton);
    
    await waitFor(() => {
      expect(screen.getByText(/hello john doe/i)).toBeInTheDocument();
    });
  });
});

// Testing custom hooks
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('increments counter', () => {
    const { result } = renderHook(() => useCounter());
    
    act(() => {
      result.current.increment();
    });
    
    expect(result.current.count).toBe(1);
  });
});

// Testing with context
import { render } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';

const renderWithTheme = (ui, { theme = 'light' } = {}) => {
  return render(
    <ThemeProvider value={theme}>
      {ui}
    </ThemeProvider>
  );
};

test('renders with dark theme', () => {
  renderWithTheme(<App />, { theme: 'dark' });
  expect(document.body).toHaveClass('dark-theme');
});

Use Cases: Component testing, user interaction testing, accessibility testing

Cypress

Description: Fast, easy and reliable testing for anything that runs in a browser with end-to-end testing capabilities.

Key Features:

  • End-to-end testing
  • Time travel debugging
  • Real browser testing
  • Network stubbing
  • Screenshot and video recording
  • Cross-browser testing
  • Component testing

Installation:

npm install --save-dev cypress
npx cypress open

Configuration (cypress.config.js):

module.exports = {
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    video: true,
    screenshotOnRunFailure: true,
  },
  component: {
    devServer: {
      framework: 'create-react-app',
      bundler: 'webpack',
    },
  },
};

Basic Usage:

// e2e tests - cypress/e2e/user-flow.cy.js
describe('User Authentication Flow', () => {
  beforeEach(() => {
    cy.visit('/');
  });

  it('should display login form', () => {
    cy.get('[data-testid="login-form"]').should('be.visible');
    cy.get('input[name="email"]').should('exist');
    cy.get('input[name="password"]').should('exist');
  });

  it('should login successfully with valid credentials', () => {
    cy.get('input[name="email"]').type('user@example.com');
    cy.get('input[name="password"]').type('password123');
    cy.get('button[type="submit"]').click();
    
    cy.url().should('include', '/dashboard');
    cy.get('[data-testid="user-menu"]').should('contain', 'Welcome');
  });

  it('should show error for invalid credentials', () => {
    cy.get('input[name="email"]').type('invalid@example.com');
    cy.get('input[name="password"]').type('wrongpassword');
    cy.get('button[type="submit"]').click();
    
    cy.get('[data-testid="error-message"]').should('be.visible');
    cy.get('[data-testid="error-message"]').should('contain', 'Invalid credentials');
  });
});

// API testing
describe('API Integration', () => {
  it('should fetch user data', () => {
    cy.intercept('GET', '/api/users/*', { fixture: 'user.json' }).as('getUser');
    
    cy.visit('/profile');
    cy.wait('@getUser');
    
    cy.get('[data-testid="user-name"]').should('contain', 'John Doe');
    cy.get('[data-testid="user-email"]').should('contain', 'john@example.com');
  });
});

// Component testing - cypress/component/Button.cy.jsx
import Button from '../../src/components/Button';

describe('Button Component', () => {
  it('renders with correct text', () => {
    cy.mount(<Button>Click me</Button>);
    cy.contains('Click me').should('be.visible');
  });

  it('handles click events', () => {
    const onClick = cy.stub();
    cy.mount(<Button onClick={onClick}>Click me</Button>);
    
    cy.get('button').click();
    cy.then(() => {
      expect(onClick).to.have.been.called;
    });
  });

  it('applies correct styling variants', () => {
    cy.mount(<Button variant="primary">Primary Button</Button>);
    cy.get('button').should('have.class', 'btn-primary');
  });
});

Use Cases: End-to-end testing, user journey testing, API integration testing

Testing Best Practices

1. Test Structure (AAA Pattern)

describe('ComponentName', () => {
  test('should do something when condition is met', () => {
    // Arrange
    const props = { title: 'Test Title' };
    
    // Act
    render(<Component {...props} />);
    
    // Assert
    expect(screen.getByText('Test Title')).toBeInTheDocument();
  });
});

2. Testing User Interactions

test('should update input value when user types', async () => {
  const user = userEvent.setup();
  render(<InputComponent />);
  
  const input = screen.getByRole('textbox');
  await user.type(input, 'Hello World');
  
  expect(input).toHaveValue('Hello World');
});

3. Testing Async Operations

test('should display data after loading', async () => {
  render(<DataComponent />);
  
  expect(screen.getByText('Loading...')).toBeInTheDocument();
  
  await waitFor(() => {
    expect(screen.getByText('Data loaded')).toBeInTheDocument();
  });
  
  expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});

4. Mocking External Dependencies

// Mock API calls
jest.mock('./api', () => ({
  fetchData: jest.fn().mockResolvedValue({ id: 1, name: 'Test' })
}));

// Mock React Router
import { MemoryRouter } from 'react-router-dom';

const renderWithRouter = (ui, { route = '/' } = {}) => {
  return render(
    <MemoryRouter initialEntries={[route]}>
      {ui}
    </MemoryRouter>
  );
};

Comparison Table

Tool Testing Level Setup Complexity Browser Support Use Case
Jest Unit/Integration Low Node.js Logic testing
React Testing Library Component Medium jsdom Component testing
Cypress E2E/Component Medium Real browsers User flow testing

When to Use Each

  • Jest: For unit testing utility functions, hooks, and business logic
  • React Testing Library: For testing React components and user interactions
  • Cypress: For end-to-end testing and testing complete user journeys

Advanced Testing Patterns

Custom Render Function

import { render } from '@testing-library/react';
import { ThemeProvider } from './ThemeProvider';
import { Router } from 'react-router-dom';

const AllTheProviders = ({ children }) => {
  return (
    <Router>
      <ThemeProvider>
        {children}
      </ThemeProvider>
    </Router>
  );
};

const customRender = (ui, options) =>
  render(ui, { wrapper: AllTheProviders, ...options });

export * from '@testing-library/react';
export { customRender as render };

Test Utilities

// test-utils.js
export const createMockUser = (overrides = {}) => ({
  id: 1,
  name: 'John Doe',
  email: 'john@example.com',
  ...overrides
});

export const renderWithUser = (ui, { user = createMockUser() } = {}) => {
  return render(
    <UserProvider value={user}>
      {ui}
    </UserProvider>
  );
};