Module 4 - Introduction to Testing

Why Testing Matters

The Importance of Testing in React Applications

Testing is an essential part of creating reliable software. Well-tested applications:

  • Have fewer bugs
  • Are easier to maintain and refactor
  • Provide documentation for how the code should behave
  • Enable confident deployment of changes

In React applications, testing becomes even more important as components interact with each other and manage state in complex ways.

Testing Frameworks

Jest

A JavaScript testing framework developed by Facebook that focuses on simplicity. Jest works out of the box with React and provides features like snapshot testing, mocking, and code coverage reports.

React Testing Library

A library for testing React components that encourages best practices by focusing on testing components as users would interact with them.

Types of Tests

Understanding Different Test Categories

There are several types of tests that serve different purposes:

  • Unit Tests: Test individual components or functions in isolation. These ensure that small pieces of code work correctly on their own.
  • Integration Tests: Test how multiple components or functions work together. These ensure that parts of your application interact correctly.
  • End-to-End Tests: Test the entire application flow from start to finish. These ensure that the application works correctly as a whole.

A well-tested application will have a mix of these different types of tests, with unit tests being the most numerous and end-to-end tests being more focused on critical user flows.


// Example Unit Test for a Button Component
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

test('calls onClick handler when clicked', () => {
  const handleClick = jest.fn();
  render(<Button onClick={handleClick}>Click Me</Button>);
  
  const button = screen.getByText('Click Me');
  fireEvent.click(button);
  
  expect(handleClick).toHaveBeenCalledTimes(1);
});
                

Writing Tests with React Testing Library

Core Principles of React Testing Library

React Testing Library is built on the principle that tests should resemble how users interact with your application. This means:

  • Focus on testing behavior rather than implementation details
  • Select elements the way users would (by text, role, etc.)
  • Interact with elements the way users would (click, type, etc.)
  • Make assertions about what users would see

// Example Form Test with React Testing Library
import { render, screen, fireEvent } from '@testing-library/react';
import LoginForm from './LoginForm';

test('submits the form with user input', () => {
  const handleSubmit = jest.fn();
  render(<LoginForm onSubmit={handleSubmit} />);
  
  // Fill out the form
  fireEvent.change(screen.getByLabelText(/username/i), { 
    target: { value: 'testuser' } 
  });
  
  fireEvent.change(screen.getByLabelText(/password/i), { 
    target: { value: 'password123' } 
  });
  
  // Submit the form
  fireEvent.click(screen.getByRole('button', { name: /log in/i }));
  
  // Assert that the form was submitted with the correct data
  expect(handleSubmit).toHaveBeenCalledWith({
    username: 'testuser',
    password: 'password123'
  });
});
                

Testing Asynchronous Code

Handling Asynchronous Operations in Tests

Modern web applications often include asynchronous operations like API calls. Testing these requires special techniques:

  • Using async/await in test functions
  • Waiting for elements to appear or disappear
  • Mocking API responses with Jest
  • Testing loading and error states

// Example Async Test
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import UserProfile from './UserProfile';

// Mock API server
const server = setupServer(
  rest.get('/api/user', (req, res, ctx) => {
    return res(ctx.json({ name: 'John Doe', email: 'john@example.com' }));
  })
);

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

test('loads and displays user data', async () => {
  render(<UserProfile userId="123" />);
  
  // Initially shows loading state
  expect(screen.getByText(/loading/i)).toBeInTheDocument();
  
  // Wait for data to load
  await waitFor(() => {
    expect(screen.getByText('John Doe')).toBeInTheDocument();
  });
  
  expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
                

Test Coverage

Measuring and Improving Test Coverage

Test coverage measures how much of your code is executed when tests run. While high coverage is generally good, it's important to focus on testing the right things rather than just increasing coverage numbers.

Jest includes built-in coverage reporting that can help identify untested code:


// Add this to package.json
{
  "scripts": {
    "test": "react-scripts test",
    "test:coverage": "react-scripts test --coverage --watchAll=false"
  },
  "jest": {
    "collectCoverageFrom": [
      "src/**/*.{js,jsx,ts,tsx}",
      "!src/**/*.d.ts",
      "!src/reportWebVitals.js",
      "!src/index.js"
    ]
  }
}
                    

Running npm run test:coverage will generate a report showing which lines of code were executed during tests, helping you identify areas that need more testing.

Module Project

Practice your testing skills by completing the Introduction to Testing project.

Project Instructions Project Repository