React Testing Library

Test React components the way users interact with them

๐Ÿงช What is React Testing Library?

React Testing Library is a testing tool that helps you test React components by simulating user interactions. It encourages testing behavior rather than implementation details, making tests more reliable and maintainable.


import { render, screen } from '@testing-library/react';

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

Result:

โœ… Test passes if "welcome" text is found in the component.

Core Testing Concepts

๐ŸŽจ

render()

Renders components for testing

const { container } = render(
  <MyComponent />
);
๐Ÿ”

screen

Queries rendered elements

const button = screen.getByRole(
  'button', { name: /submit/i }
);
๐Ÿ‘†

userEvent

Simulates user interactions

await userEvent.click(button);
await userEvent.type(input, 'text');
โœ…

Assertions

Verifies expected behavior

expect(element)
  .toBeInTheDocument();

๐Ÿ”น Basic Component Test

Start with a simple test that renders a component and checks if text appears. This is the foundation of React Testing Library.

// Button.jsx
import React from 'react';

function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

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

test('renders button with text', () => {
  // Render the component
  render(<Button>Click Me</Button>);
  
  // Find the button
  const button = screen.getByText('Click Me');
  
  // Assert it exists
  expect(button).toBeInTheDocument();
});

Output:

โœ… PASS: Button renders with correct text

๐Ÿ”น Testing User Interactions

Test how components respond to user actions like clicks, typing, and form submissions. Use userEvent for realistic user interactions.

// Counter.jsx
import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

export default Counter;
// Counter.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';

test('increments count on button click', async () => {
  // Setup user event
  const user = userEvent.setup();
  
  // Render component
  render(<Counter />);
  
  // Find elements
  const button = screen.getByRole('button', { name: /increment/i });
  const count = screen.getByText(/count: 0/i);
  
  // Initial state
  expect(count).toBeInTheDocument();
  
  // Click button
  await user.click(button);
  
  // Check updated state
  expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});

Output:

โœ… PASS: Counter increments from 0 to 1 on click

๐Ÿ”น Query Methods

React Testing Library provides different query methods to find elements. Choose the right query based on what you're testing and how users interact with your app.

Query Types:

  • getBy... - Returns element or throws error (use for elements that should exist)
  • queryBy... - Returns element or null (use to check if element doesn't exist)
  • findBy... - Returns promise (use for async elements)
import { render, screen } from '@testing-library/react';

test('demonstrates query methods', async () => {
  render(<MyComponent />);
  
  // getByRole - Best for accessibility
  const button = screen.getByRole('button', { name: /submit/i });
  
  // getByText - Find by visible text
  const heading = screen.getByText('Welcome');
  
  // getByLabelText - For form inputs
  const input = screen.getByLabelText('Email');
  
  // getByPlaceholderText - By placeholder
  const search = screen.getByPlaceholderText('Search...');
  
  // queryByText - Returns null if not found
  const missing = screen.queryByText('Not Here');
  expect(missing).not.toBeInTheDocument();
  
  // findByText - Wait for async element
  const async = await screen.findByText('Loaded Data');
});

Output:

Use getByRole for best accessibility and maintainability.

๐Ÿ”น Testing Forms

Forms are common in React apps. Test form inputs, validation, and submission to ensure they work correctly for users.

// LoginForm.jsx
import React, { useState } from 'react';

function LoginForm({ onSubmit }) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    onSubmit({ email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">Email</label>
      <input
        id="email"
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      
      <label htmlFor="password">Password</label>
      <input
        id="password"
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      
      <button type="submit">Login</button>
    </form>
  );
}
// LoginForm.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';

test('submits form with email and password', async () => {
  const user = userEvent.setup();
  const mockSubmit = jest.fn();
  
  render(<LoginForm onSubmit={mockSubmit} />);
  
  // Find form elements
  const emailInput = screen.getByLabelText(/email/i);
  const passwordInput = screen.getByLabelText(/password/i);
  const submitButton = screen.getByRole('button', { name: /login/i });
  
  // Fill out form
  await user.type(emailInput, '[email protected]');
  await user.type(passwordInput, 'password123');
  
  // Submit form
  await user.click(submitButton);
  
  // Verify submission
  expect(mockSubmit).toHaveBeenCalledWith({
    email: '[email protected]',
    password: 'password123'
  });
});

Output:

โœ… PASS: Form submits with correct email and password values

๐Ÿ”น Testing Async Operations

Many components fetch data or perform async operations. Use findBy queries and waitFor to test async behavior properly.

// UserProfile.jsx
import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  
  return <div>{user.name}</div>;
}
// UserProfile.test.jsx
import { render, screen } from '@testing-library/react';
import UserProfile from './UserProfile';

// Mock fetch
global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ name: 'John Doe' })
  })
);

test('displays user name after loading', async () => {
  render(<UserProfile userId={1} />);
  
  // Check loading state
  expect(screen.getByText(/loading/i)).toBeInTheDocument();
  
  // Wait for user name to appear
  const userName = await screen.findByText('John Doe');
  expect(userName).toBeInTheDocument();
  
  // Loading should be gone
  expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});

Output:

โœ… PASS: Component shows loading, then displays user name

๐Ÿง  Test Your Knowledge

Which query should you use to find an element that might not exist?