Unit Testing in React

Unit Testing in React – Complete Guide with Examples (2025)

The most popular and recommended way to unit test React components today is using React Testing Library (RTL) along with Jest.

React Testing Library focuses on testing components the way users interact with them (by accessibility, labels, text, roles — not by implementation details).

1. Basic Setup (create-react-app or Vite)

Bash

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

Add to your setupTests.js or vitest.setup.ts:

JavaScript

import '@testing-library/jest-dom'

2. Simple Example: Testing a Counter Component

jsx

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

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

  return (
    <div>
      <h1 data-testid="title">My Counter</h1>
      <p aria-label="count">Current count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
      <button onClick={() => setCount(c => c - 1)}>Decrement</button>
    </div>
  );
}

jsx

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

test('renders counter and increments/decrements correctly', async () => {
  const user = userEvent.setup();
  
  render(<Counter />);

  // 1. By visible text
  expect(screen.getByText('My Counter')).toBeInTheDocument();
  expect(screen.getByText(/current count: 0/i)).toBeInTheDocument();

  // 2. By role + name (recommended)
  const incrementBtn = screen.getByRole('button', { name: /increment/i });
  const decrementBtn = screen.getByRole('button', { name: /decrement/i });

  await user.click(incrementBtn);
  await user.click(incrementBtn);
  expect(screen.getByText(/current count: 2/i)).toBeInTheDocument();

  await user.click(decrementBtn);
  expect(screen.getByText(/current count: 1/i)).toBeInTheDocument();
});

All Ways to Query / Identify Elements in React Testing Library

Query TypeRecommendedExampleWhen to Use
getByRole★★★★★screen.getByRole(‘button’, { name: ‘Submit’ })Most cases – accessible
getByLabelText★★★★★screen.getByLabelText(‘Username:’)Form fields
getByPlaceholderText★★★★screen.getByPlaceholderText(‘Enter email’)When no label
getByText★★★★screen.getByText(‘Welcome, John’)Visible text
getByDisplayValue★★★★screen.getByDisplayValue(‘john@example.com’)Input values
getByAltText★★★★★screen.getByAltText(‘Profile picture’)Images
getByTitle★★★★screen.getByTitle(‘Close’)Tooltips
getByTestId★★screen.getByTestId(‘custom-id’) → <div data-testid=”custom-id”>Last resort
getByAriaLabel (custom)Use getByRole instead whenever possible

Query Variants (important!)

For every query above, there are 4 variants:

VariantThrows if…ReturnsUse When
getBy…0 or >1 elements1 elementExpect exactly one
getAllBy…0 elementsArrayMultiple elements
queryBy…Never throws (returns null)Element or nullCheck absence
queryAllBy…Never throwsArray (empty if none)Find multiple or none
findBy…Not found within timeout (async)Promise → elementAsync content (API calls)
findAllBy…Not found within timeoutPromise → arrayMultiple async

Best Practices (2025)

JavaScript

// GOOD
screen.getByRole('button', { name: 'Save' })
screen.getByLabelText('Password')

// AVOID (tests become brittle)
screen.getByTestId('submit-btn')   // only when no accessible way
container.querySelector('.css-abc') // never

Testing Custom Hooks (with @testing-library/react-hooks)

Bash

npm install --save-dev @testing-library/react-hooks

JavaScript

// useCounter.js
import { useState } from 'react';
export function useCounter(initial = 0) {
  const [count, setCount] = useState(initial);
  return { count, increment: () => setCount(c => c + 1), decrement: () => setCount(c => c - 1) };
}

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

test('should increment and decrement', () => {
  const { result } = renderHook(() => useCounter(10));

  act(() => result.current.increment());
  expect(result.current.count).toBe(11);

  act(() => result.current.decrement());
  expect(result.current.count).toBe(10);
});

Summary: Prefer queries in this order

  1. getByRole
  2. getByLabelText
  3. getByPlaceholderText
  4. getByText
  5. getByDisplayValue
  6. getByAltText
  7. getByTitle
  8. getByTestId → only when nothing else works

This approach makes your tests resilient to refactoring and mirrors real user behavior.

  • @testing-library/jest-dom matchers (DOM-specific, by far the most common in modern React projects)
  • A few essential built-in Jest matchers that pair with them

Top 10–12 Most Frequently Used Matchers (with short explanations)

  1. .toBeInTheDocument()
    Checks if the element exists in the DOM.
    expect(screen.getByText('Welcome')).toBeInTheDocument()
  2. .toHaveTextContent('text' | /regex/)
    Verifies the visible text content (very common).
    expect(button).toHaveTextContent('Submit')
    expect(card).toHaveTextContent(/price: \$?99/i)
  3. .toBeVisible()
    Element is visible (not hidden by CSS, opacity, visibility, etc.).
    → Great for conditional rendering checks.
  4. .toBeDisabled() / .toBeEnabled()
    Checks disabled/enabled state of buttons, inputs, etc. (form + UX testing).
    expect(submitBtn).toBeDisabled()
  5. .toHaveClass('class-name')
    Verifies CSS class presence (styling/state).
    expect(element).toHaveClass('active')
    → Can take multiple: .toHaveClass('btn', 'primary')
  6. .toHaveAttribute('attr', 'value?')
    Checks for attributes (very common with data-testid, aria-*, etc.).
    expect(input).toHaveAttribute('placeholder', 'Enter name')
  7. .toHaveValue('value')
    Checks form field value (input, textarea, select).
    expect(input).toHaveValue('john@example.com')
  8. .toBeChecked() / .toBePartiallyChecked()
    For checkboxes, radios, switches.
    expect(checkbox).toBeChecked()
  9. .toHaveFocus()
    Element has focus (after interactions).
    expect(input).toHaveFocus()
  10. .toBeRequired()
    Form field is required.
    expect(input).toBeRequired()
  11. .toHaveAccessibleName('name')
    Accessibility: element has correct accessible name (button, input, etc.).
    → Increasingly important in 2025–2026 projects.
  12. .toHaveStyle({ color: 'red' })
    Inline or computed style check.
    expect(el).toHaveStyle({ display: 'none' })

Bonus: Still-essential built-in Jest matchers (used together with the above)

  • .toEqual(obj/array) → deep equality (props, state, returned values)
  • .toBe(value) → strict equality (primitives)
  • .toHaveBeenCalledWith(...) → mock function calls (event handlers)
  • .not. prefix → negation (.not.toBeDisabled(), .not.toBeInTheDocument())

Quick “Top 5” cheat-sheet most devs use daily

expect(el).toBeInTheDocument()
expect(el).toHaveTextContent('...')
expect(btn).toBeEnabled()     // or .toBeDisabled()
expect(input).toHaveValue('...')
expect(el).toHaveAttribute('data-testid', 'hero')

These cover the vast majority of assertions in component + form + accessibility tests.

If you’re starting fresh in 2026, install @testing-library/jest-dom and add to your setup:

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

There are two main groups:

  1. Built-in Jest matchers (available out of the box with Jest)
  2. @testing-library/jest-dom matchers (very common in React/RTL projects — you usually add import '@testing-library/jest-dom' in your setup file)

1. Core Jest Matchers (most frequently used)

expect(value).toBe(5)                // strict equality (===)
expect(value).toEqual({ a: 1 })     // deep equality
expect(value).toBeTruthy()           // true, 1, "hello", objects, arrays...
expect(value).toBeFalsy()            // false, 0, "", null, undefined...
expect(value).toBeNull()
expect(value).toBeUndefined()
expect(value).toBeDefined()

expect(value).toBeGreaterThan(10)
expect(value).toBeGreaterThanOrEqual(10)
expect(value).toBeLessThan(100)
expect(value).toBeLessThanOrEqual(100)

expect(value).toMatch(/regex/)       // string regex match
expect(value).toMatch('substring')   // string contains
expect(value).toContain('item')      // array contains item
expect(array).toContainEqual({ x: 1 }) // deep equality in array

expect(value).toHaveLength(3)        // string / array length
expect(object).toHaveProperty('name')

expect(fn).toThrow()                 // throws any error
expect(fn).toThrow('specific message')
expect(fn).toThrow(Error)

expect(mockFn).toHaveBeenCalled()
expect(mockFn).toHaveBeenCalledTimes(2)
expect(mockFn).toHaveBeenCalledWith('arg1', 42)
expect(mockFn).toHaveBeenLastCalledWith(...)
expect(mockFn).toHaveBeenNthCalledWith(2, ...)

expect(value).not.toBe(5)            // negation (works with almost all matchers)

2. DOM-specific matchers from @testing-library/jest-dom

(these are the ones people usually mean when testing React components)

// Most popular ones
expect(element).toBeInTheDocument()
expect(element).not.toBeInTheDocument()

expect(element).toBeVisible()       // not hidden via CSS
expect(element).toBeHidden()

expect(element).toHaveTextContent('hello world')
expect(element).toHaveTextContent(/hello/i)     // regex
expect(element).toHaveTextContent('exact', { normalizeWhitespace: false })

expect(element).toContainElement(childElement)

expect(element).toHaveClass('btn')
expect(element).toHaveClass('primary', 'large')   // multiple classes
expect(element).not.toHaveClass('disabled')

expect(element).toHaveAttribute('data-testid', 'submit-btn')
expect(element).toHaveAttribute('disabled')

expect(element).toBeDisabled()
expect(element).toBeEnabled()

expect(element).toBeRequired()
expect(element).toBeChecked()       // checkboxes & radio
expect(element).toBePartiallyChecked()

expect(element).toHaveFocus()

expect(element).toHaveStyle({ color: 'red', fontSize: '16px' })
expect(element).toHaveStyle('color: red')   // string form

expect(element).toHaveFormValue('username', 'john')   // form fields

Quick reference – most common pattern in React Testing Library

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

test('shows button', () => {
  render(<Button>Click me</Button>)

  const btn = screen.getByRole('button', { name: /click me/i })

  expect(btn).toBeInTheDocument()
  expect(btn).toBeEnabled()
  expect(btn).toHaveClass('btn-primary')
  expect(btn).toHaveTextContent('Click me')
})

Summary – the ones you’ll use 90% of the time

  • .toBeInTheDocument()
  • .toHaveTextContent()
  • .toBeVisible()
  • .toHaveClass()
  • .toBeDisabled()
  • .toBeEnabled()
  • .toBeChecked()
  • .toHaveAttribute()
  • .toHaveStyle()
  • .toBe() / .toEqual()
  • .toHaveBeenCalledWith()

For the complete up-to-date list:

  • Jest built-in → https://jestjs.io/docs/expect
  • jest-dom matchers → https://github.com/testing-library/jest-dom

Happy testing

Tags: No tags

Add a Comment

Your email address will not be published. Required fields are marked *