Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
LoginSign Up
menu
  1. Home
  2. /Web Development
  3. /React
  4. /Testing React Components
Your Progress0%
0 of 42 completed

React Topics

Getting Started

  • What is React?
  • React vs Vanilla JavaScript
  • Setting Up Your Environment
  • Your First React App

JSX Fundamentals

  • Introduction to JSX
  • JSX Expressions and Variables
  • Conditional Rendering
  • Lists and Keys
  • Styling in JSX

Components Basics

  • Function Components
  • Props
  • Children Prop
  • Props Destructuring
  • Component Composition

State Management

  • Introduction to State
  • useState Hook
  • State Updates and Re-renders
  • Multiple State Variables
  • State Best Practices

Events and Interactivity

  • Handling Events
  • Event Objects
  • Forms in React
  • Form Validation

Side Effects and Data

  • Introduction to useEffect
  • Data Fetching
  • Cleanup Functions
  • Dependency Arrays

Advanced Hooks

  • useRef Hook
  • useContext Hook
  • useMemo Hook
  • useCallback Hook

React Patterns

  • Custom Hooks
  • Higher-Order Components
  • Render Props Pattern

React Router

  • Introduction to React Router
  • Routes and Navigation
  • Dynamic Routes and Parameters

Best Practices

  • Component Organization
  • Performance Optimization
  • Error Handling
  • Testing React Components

Real World Project

  • Building a Complete App

Testing React Components

Writing reliable tests with Jest and React Testing Library

Testing ensures your React components work correctly and continue working as your app evolves. Without tests, every change risks breaking something. With tests, you can refactor confidently, catch bugs early, and ship reliable code. In this lesson, you'll learn how to test React components using Jest (test runner) and React Testing Library (testing utilities). Let's build confidence in your code! ๐Ÿงช

Why Test React Components?

  • Catch bugs early - Before users find them
  • Refactor confidently - Tests catch breaking changes
  • Document behavior - Tests show how components should work
  • Prevent regressions - Old bugs stay fixed
  • Faster development - Less manual testing
  • Better design - Testable code is better code

Testing Pyramid:

Unit Tests (70%): Test individual components in isolation
Integration Tests (20%): Test components working together
End-to-End Tests (10%): Test complete user flows

Focus on unit and integration tests for React components!

Setup: Jest and React Testing Library

Create React App (Built-in)

cra-setup.sh
# Create React App includes testing setup automatically!
npx create-react-app my-app

# Already includes:
# - Jest (test runner)
# - React Testing Library
# - @testing-library/jest-dom (matchers)

# Run tests
npm test

# Watch mode starts automatically
# Press 'a' to run all tests
# Press 'q' to quit

Manual Setup (Vite, Next.js, etc.)

manual-setup.sh
# Install dependencies
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event

# For Vite/ES modules
npm install --save-dev @vitejs/plugin-react jsdom

# Create jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  moduleNameMapper: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
  },
}

# Create jest.setup.js
import '@testing-library/jest-dom'

# Add to package.json
"scripts": {
  "test": "jest",
  "test:watch": "jest --watch"
}

Your First Test

Simple Component

Button.jsx
// Button.jsx
function Button({ children, onClick, disabled }) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {children}
    </button>
  )
}

export default Button

Simple Test

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

// Test suite
describe('Button', () => {
  // Individual test
  test('renders button with text', () => {
    render(<Button>Click me</Button>)
    
    const button = screen.getByRole('button', { name: /click me/i })
    expect(button).toBeInTheDocument()
  })
  
  test('calls onClick when clicked', async () => {
    const handleClick = jest.fn() // Mock function
    render(<Button onClick={handleClick}>Click me</Button>)
    
    const button = screen.getByRole('button')
    await userEvent.click(button)
    
    expect(handleClick).toHaveBeenCalledTimes(1)
  })
  
  test('button is disabled when disabled prop is true', () => {
    render(<Button disabled>Click me</Button>)
    
    const button = screen.getByRole('button')
    expect(button).toBeDisabled()
  })
})

// Run: npm test
// Result: โœ“ 3 tests passed

Test Anatomy

test-anatomy.jsx
import { render, screen } from '@testing-library/react'

test('description of what you are testing', () => {
  // 1. ARRANGE - Set up the test
  render(<Component prop="value" />)
  
  // 2. ACT - Perform actions
  const button = screen.getByRole('button')
  await userEvent.click(button)
  
  // 3. ASSERT - Check results
  expect(button).toBeInTheDocument()
})

// Pattern: Arrange โ†’ Act โ†’ Assert (AAA)

Querying Elements

Query Types

query-types.jsx
import { render, screen } from '@testing-library/react'

render(<MyComponent />)

// getBy* - Returns element or throws error (element must exist)
const button = screen.getByRole('button')
const heading = screen.getByText('Hello')
const input = screen.getByLabelText('Email')

// queryBy* - Returns element or null (element may not exist)
const optional = screen.queryByText('Optional')
expect(optional).toBeNull() // Use for asserting non-existence

// findBy* - Returns promise (for async elements)
const async = await screen.findByText('Loaded')

// *AllBy* - Returns array of elements
const buttons = screen.getAllByRole('button')
expect(buttons).toHaveLength(3)

// Priority order (best to worst):
// 1. getByRole - Most accessible
// 2. getByLabelText - For forms
// 3. getByPlaceholderText
// 4. getByText
// 5. getByTestId - Last resort

Common Queries

common-queries.jsx
// By Role (BEST - matches accessibility)
screen.getByRole('button', { name: /submit/i })
screen.getByRole('heading', { level: 1 })
screen.getByRole('textbox', { name: /email/i })

// By Label (Good for forms)
screen.getByLabelText('Email')
screen.getByLabelText(/password/i)

// By Placeholder
screen.getByPlaceholderText('Enter email')

// By Text
screen.getByText('Welcome')
screen.getByText(/welcome/i) // Case insensitive regex

// By Test ID (Last resort)
screen.getByTestId('custom-element')
// <div data-testid="custom-element">

// Multiple elements
screen.getAllByRole('listitem')
screen.getAllByText(/item/i)

// With within for scoping
import { within } from '@testing-library/react'
const list = screen.getByRole('list')
const items = within(list).getAllByRole('listitem')

Testing User Interactions

Click Events

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

function Counter() {
  const [count, setCount] = React.useState(0)
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

test('increments count when button clicked', async () => {
  render(<Counter />)
  
  const button = screen.getByRole('button', { name: /increment/i })
  expect(screen.getByText(/count: 0/i)).toBeInTheDocument()
  
  // Click button
  await userEvent.click(button)
  
  expect(screen.getByText(/count: 1/i)).toBeInTheDocument()
  
  // Click again
  await userEvent.click(button)
  expect(screen.getByText(/count: 2/i)).toBeInTheDocument()
})

Form Inputs

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

function LoginForm({ onSubmit }) {
  const [email, setEmail] = React.useState('')
  const [password, setPassword] = React.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">Log In</button>
    </form>
  )
}

test('submits form with email and password', async () => {
  const handleSubmit = jest.fn()
  render(<LoginForm onSubmit={handleSubmit} />)
  
  // Type in inputs
  const emailInput = screen.getByLabelText(/email/i)
  const passwordInput = screen.getByLabelText(/password/i)
  
  await userEvent.type(emailInput, 'test@example.com')
  await userEvent.type(passwordInput, 'password123')
  
  // Submit form
  const submitButton = screen.getByRole('button', { name: /log in/i })
  await userEvent.click(submitButton)
  
  // Assert
  expect(handleSubmit).toHaveBeenCalledTimes(1)
  expect(handleSubmit).toHaveBeenCalledWith({
    email: 'test@example.com',
    password: 'password123'
  })
})

Async Operations

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

function UserProfile({ userId }) {
  const [user, setUser] = React.useState(null)
  const [loading, setLoading] = React.useState(true)
  
  React.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: {user.name}</div>
}

test('displays user after loading', async () => {
  // Mock fetch
  global.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve({ name: 'John Doe' })
    })
  )
  
  render(<UserProfile userId={1} />)
  
  // Initially shows loading
  expect(screen.getByText(/loading/i)).toBeInTheDocument()
  
  // Wait for user to appear
  const userName = await screen.findByText(/john doe/i)
  expect(userName).toBeInTheDocument()
  
  // Or use waitFor:
  await waitFor(() => {
    expect(screen.getByText(/john doe/i)).toBeInTheDocument()
  })
})

Mocking

Mock Functions

mock-functions.jsx
// Create mock function
const mockFn = jest.fn()

// Mock with return value
const mockFn = jest.fn(() => 'returned value')
mockFn.mockReturnValue('returned value')

// Mock with different returns
mockFn.mockReturnValueOnce('first').mockReturnValueOnce('second')

// Mock async
mockFn.mockResolvedValue({ data: 'value' })
mockFn.mockRejectedValue(new Error('Failed'))

// Test the mock
expect(mockFn).toHaveBeenCalled()
expect(mockFn).toHaveBeenCalledTimes(2)
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2')
expect(mockFn).toHaveBeenLastCalledWith('lastArg')

// Get call data
expect(mockFn.mock.calls[0][0]).toBe('first argument')
expect(mockFn.mock.results[0].value).toBe('return value')

Mock API Calls

mock-api.test.jsx
// Option 1: Mock fetch globally
beforeEach(() => {
  global.fetch = jest.fn()
})

afterEach(() => {
  jest.resetAllMocks()
})

test('fetches users', async () => {
  const mockUsers = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
  ]
  
  global.fetch.mockResolvedValueOnce({
    ok: true,
    json: async () => mockUsers
  })
  
  render(<UserList />)
  
  const users = await screen.findAllByRole('listitem')
  expect(users).toHaveLength(2)
})

// Option 2: Mock module
jest.mock('./api', () => ({
  fetchUsers: jest.fn(() => Promise.resolve([
    { id: 1, name: 'Alice' }
  ]))
}))

// Option 3: Use MSW (Mock Service Worker) for more realistic mocking
import { rest } from 'msw'
import { setupServer } from 'msw/node'

const server = setupServer(
  rest.get('/api/users', (req, res, ctx) => {
    return res(ctx.json([{ id: 1, name: 'Alice' }]))
  })
)

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

Common Testing Patterns

Testing Conditional Rendering

conditional-rendering.test.jsx
function Greeting({ isLoggedIn, username }) {
  if (isLoggedIn) {
    return <div>Welcome back, {username}!</div>
  }
  return <div>Please log in</div>
}

test('shows welcome message when logged in', () => {
  render(<Greeting isLoggedIn={true} username="Alice" />)
  expect(screen.getByText(/welcome back, alice/i)).toBeInTheDocument()
})

test('shows login prompt when not logged in', () => {
  render(<Greeting isLoggedIn={false} />)
  expect(screen.getByText(/please log in/i)).toBeInTheDocument()
  expect(screen.queryByText(/welcome back/i)).not.toBeInTheDocument()
})

Testing Lists

list-testing.test.jsx
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          {todo.text}
          {todo.completed && <span>โœ“</span>}
        </li>
      ))}
    </ul>
  )
}

test('renders all todos', () => {
  const todos = [
    { id: 1, text: 'Buy milk', completed: false },
    { id: 2, text: 'Walk dog', completed: true }
  ]
  
  render(<TodoList todos={todos} />)
  
  const items = screen.getAllByRole('listitem')
  expect(items).toHaveLength(2)
  expect(screen.getByText(/buy milk/i)).toBeInTheDocument()
  expect(screen.getByText(/walk dog/i)).toBeInTheDocument()
})

test('shows checkmark for completed todos', () => {
  const todos = [
    { id: 1, text: 'Done task', completed: true }
  ]
  
  render(<TodoList todos={todos} />)
  expect(screen.getByText('โœ“')).toBeInTheDocument()
})

Testing Custom Hooks

hook-testing.test.jsx
import { renderHook, act } from '@testing-library/react'

function useCounter(initialValue = 0) {
  const [count, setCount] = React.useState(initialValue)
  
  const increment = () => setCount(c => c + 1)
  const decrement = () => setCount(c => c - 1)
  const reset = () => setCount(initialValue)
  
  return { count, increment, decrement, reset }
}

test('useCounter increments count', () => {
  const { result } = renderHook(() => useCounter())
  
  expect(result.current.count).toBe(0)
  
  act(() => {
    result.current.increment()
  })
  
  expect(result.current.count).toBe(1)
})

test('useCounter starts with initial value', () => {
  const { result } = renderHook(() => useCounter(10))
  expect(result.current.count).toBe(10)
})

test('useCounter resets to initial value', () => {
  const { result } = renderHook(() => useCounter(5))
  
  act(() => {
    result.current.increment()
    result.current.increment()
  })
  
  expect(result.current.count).toBe(7)
  
  act(() => {
    result.current.reset()
  })
  
  expect(result.current.count).toBe(5)
})

Testing Best Practices

  1. Test Behavior, Not Implementation
test-behavior.test.jsx
// โŒ Bad - Tests implementation details
test('updates state when button clicked', () => {
  const { result } = renderHook(() => useCounter())
  act(() => result.current.increment())
  expect(result.current.count).toBe(1) // Testing internal state
})

// โœ… Good - Tests user-visible behavior
test('shows incremented count when button clicked', async () => {
  render(<Counter />)
  const button = screen.getByRole('button', { name: /increment/i })
  
  await userEvent.click(button)
  
  expect(screen.getByText(/count: 1/i)).toBeInTheDocument()
})

// Test what users see, not how it works internally!
  1. Use Accessible Queries
accessible-queries.test.jsx
// โŒ Bad - Non-semantic queries
screen.getByTestId('submit-button')
screen.getByClassName('btn-primary')

// โœ… Good - Semantic, accessible queries
screen.getByRole('button', { name: /submit/i })
screen.getByLabelText(/email/i)
screen.getByText(/welcome/i)

// Benefits:
// - Tests accessibility
// - More resilient to changes
// - Matches how users interact
  1. Write Descriptive Test Names
test-names.test.jsx
// โŒ Bad - Vague test names
test('button works', () => {})
test('test 1', () => {})

// โœ… Good - Clear, specific names
test('increments counter when increment button is clicked', () => {})
test('shows error message when form submitted with empty email', () => {})
test('disables submit button while loading', () => {})

// Good test names:
// - Describe what is being tested
// - Describe the action/condition
// - Describe expected outcome
  1. Keep Tests Independent
independent-tests.test.jsx
// โŒ Bad - Tests depend on each other
let user
test('creates user', () => {
  user = { id: 1, name: 'Alice' }
})
test('updates user', () => {
  user.name = 'Bob' // Depends on previous test!
})

// โœ… Good - Each test is independent
test('creates user', () => {
  const user = { id: 1, name: 'Alice' }
  expect(user.name).toBe('Alice')
})

test('updates user name', () => {
  const user = { id: 1, name: 'Alice' }
  user.name = 'Bob'
  expect(user.name).toBe('Bob')
})

// Each test should work in isolation!
  1. Don't Test External Libraries
dont-test-libraries.test.jsx
// โŒ Bad - Testing React Router
test('Link navigates correctly', () => {
  // Don't test that Link works - it's already tested!
})

// โœ… Good - Test your code that uses the library
test('clicking logo navigates to home', async () => {
  render(<Header />)
  const logo = screen.getByRole('link', { name: /home/i })
  expect(logo).toHaveAttribute('href', '/')
})

// Trust libraries are tested. Test your usage!

Test Coverage

coverage.sh
# Run tests with coverage
npm test -- --coverage

# Or add to package.json
"scripts": {
  "test:coverage": "jest --coverage"
}

# Coverage report shows:
# - % of statements covered
# - % of branches covered
# - % of functions covered
# - % of lines covered

# View detailed report
open coverage/lcov-report/index.html

# Set coverage thresholds in jest.config.js
module.exports = {
  coverageThreshold: {
    global: {
      statements: 80,
      branches: 80,
      functions: 80,
      lines: 80
    }
  }
}

# Aim for 80%+ coverage, but 100% not always necessary!

Key Takeaways

  • Tests catch bugs early and enable confident refactoring
  • Use Jest as test runner, React Testing Library for utilities
  • Follow Arrange โ†’ Act โ†’ Assert pattern
  • Use getByRole for accessible queries
  • Use userEvent for realistic user interactions
  • Mock API calls with jest.fn()
  • Test behavior users see, not implementation details
  • Write descriptive test names
  • Keep tests independent and isolated
  • Use findBy for async elements
  • Don't test external libraries
  • Aim for 80%+ test coverage
  • Run tests in watch mode during development

What's Next?

Excellent work! You now know how to test React components effectively with Jest and React Testing Library. You can write reliable, maintainable tests that give you confidence in your code.

In the final lesson, you'll put everything together by Building a Complete App:

  • Multi-page application with routing
  • State management across components
  • API integration and data fetching
  • Form handling and validation
  • Error handling and loading states
  • Performance optimization
  • Component testing

Get ready to build a real-world React application from scratch! ๐Ÿš€

Just completed the React tutorial series! Learning testing with Jest and React Testing Library.

Previous
Error Handling
Next
Building a Complete App

Never Miss a New React Tutorial

Join 2,000+ developers learning React step-by-step. Get new tutorials, tips, and exclusive resources delivered to your inbox - completely FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

React Tutorials

0 of 42 completed

Your Progress0%

Topics

Getting Started

  • What is React?
  • React vs Vanilla JavaScript
  • Setting Up Your Environment
  • Your First React App

JSX Fundamentals

  • Introduction to JSX
  • JSX Expressions and Variables
  • Conditional Rendering
  • Lists and Keys
  • Styling in JSX

Components Basics

  • Function Components
  • Props
  • Children Prop
  • Props Destructuring
  • Component Composition

State Management

  • Introduction to State
  • useState Hook
  • State Updates and Re-renders
  • Multiple State Variables
  • State Best Practices

Events and Interactivity

  • Handling Events
  • Event Objects
  • Forms in React
  • Form Validation

Side Effects and Data

  • Introduction to useEffect
  • Data Fetching
  • Cleanup Functions
  • Dependency Arrays

Advanced Hooks

  • useRef Hook
  • useContext Hook
  • useMemo Hook
  • useCallback Hook

React Patterns

  • Custom Hooks
  • Higher-Order Components
  • Render Props Pattern

React Router

  • Introduction to React Router
  • Routes and Navigation
  • Dynamic Routes and Parameters

Best Practices

  • Component Organization
  • Performance Optimization
  • Error Handling
  • Testing React Components

Real World Project

  • Building a Complete App
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

ยฉ 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo