What is the React Testing Library?
The React Testing Library has special tools to test how your website looks and behaves from a user’s perspective. It checks if buttons work when clicked and if input boxes accept text correctly.
Instead of testing everything by hand, which can be slow and might miss things as your website scales up, you can write tests and run them all at once with just one command.
Component Testing Principles
What to Test | What Not to Test |
Make sure elements are present/absent in the document, Confirm expected behaviors for user actions like clicking a button or updating text in an input box and so on | Internal state of a component, Internal methods of a component, Lifecycle methods of a component, Child components |
Recommended Best Practices
- Act like a user would. Instead of using fireEvent(), use “@testing-library/user-event”. it offers an experience closer to that of a real user’s interactions with clicking and other actions
- Don’t make a separate example app for testing components. Just import the component directly and test it.
- Keep your test files alongside your components. For example, if you have a file named example.jsx, create a test file named example.test.jsx in the same directory. This makes it easier to track test coverage over time.
- Follow test-driven development (TDD): Write your tests first, then create the component.
Install Dependencies
To start, install the dependencies related to React Testing Library:
npm install -D @testing-library/jest-dom @testing-library/react @testing-library/user-event @types/jest @types/testing-library__jest-dom @vitejs/plugin-react jsdom vite vitest
After running the above command, these devDependencies should be automatically added to the package.json file.
// package.json
"devDependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/testing-library__jest-dom": "^5.14.8",
"@vitejs/plugin-react": "^4.2.1",
"jsdom": "^24.0.0",
"vite": "^5.1.3",
"vitest": "^1.3.1"
}
Configuration
Create a file named vite.config.js in the root directory. The code below shows the very minimum configuration needed for setting up React Testing Library + Vitest.
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
},
})
JavaScriptCode Example
Instead of long explanations, skilled developers grasp concepts better through real code examples.
Basic Example:
import { expect, describe, test, vi } from 'vitest'
import { render, screen, act } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom/extend-expect'
import App from './App'
describe('Heading component', () => {
test('should render App component correctly', () => {
render(<App />)
act(() => {
const btn = screen.getByRole('button', {
name: /increase/i,
})
userEvent.click(btn)
})
const outputDiv = screen.getByText(/Zoo has 10 bears\./i)
expect(outputDiv).to.exist
})
})
JavaScriptimport { render, screen } from '@testing-library/react'
import { describe, test } from 'vitest'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom/extend-expect'
import Register from './Register'
describe('Register component', () => {
it('should render Register component correctly',
() => {
render(<Register />)
const element = screen.getByRole('heading', {
level: 2,
})
expect(element).toBeInTheDocument()
})
it('should test for presence of subheading in the component',
() => {
render(<Register />)
const element = screen.getByRole('heading', {
name: /please enter your details below to register yourself\./i,
})
expect(element).toBeInTheDocument()
})
it('should show error message when all the fields are not entered',
async () => {
render(<Register />)
const buttonElement = screen.getByRole('button', {
name: /register/i,
})
await userEvent.click(buttonElement)
const alertElement = screen.getByRole('alert')
expect(alertElement).toBeInTheDocument()
})
it('should not show any error message when the component is loaded',
() => {
render(<Register />)
const alertElement = screen.queryByRole('alert')
expect(alertElement).not.toBeInTheDocument()
})
it('should show success message when the registration is successful.',
async () => {
render(<Register />)
const buttonElement = screen.getByRole('button', {
name: /register/i,
})
await userEvent.click(buttonElement)
const alertElement = screen.getByRole('alert')
expect(alertElement).toBeInTheDocument()
})
})
JavaScriptExample with Mocks (.tsx)
import Heading from './Heading'
import { expect, describe, test, vi } from 'vitest'
import { render, screen, act } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom/extend-expect'
// Test suite for the Heading component
describe('Heading component', () => {
// Spies
let onCloseSpy: jest.SpyInstance<void, []> | null
let onEditSpy: jest.SpyInstance<void, [string]> | null
// Set up before each test
beforeEach(async () => {
// Mock necessary modules and functions
vi.mock('../init', () => {
return {
actions: {
onClose: () => {},
onEdit: (src: string) => 'FAKE_IMAGE_URL',
},
}
})
vi.mock('../store', () => {
return {
setStatus: (status: Boolean) => {},
}
})
vi.mock('./crop', () => {
return {
getData: () => {},
}
})
// Import the module and set up spies
const actionsModule = await import('./init')
onCloseSpy = vi.spyOn(actionsModule.actions, 'onClose')
onEditSpy = vi.spyOn(actionsModule.actions, 'onEdit')
})
// Clean up after each test
afterEach(() => {
vi.restoreAllMocks()
})
// Test case: should render Heading component correctly
test('should render Heading component correctly', () => {
// Render the Heading component
render(<Heading />)
// Assert that the necessary elements are rendered
const heading = screen.getByRole('heading', {
name: 'Image Section',
})
expect(heading).toBeInTheDocument()
const cancelButton = screen.getByRole('button', {
name: 'Cancel',
})
expect(cancelButton).toBeInTheDocument()
const saveButton = screen.getByRole('button', {
name: 'Save',
})
expect(saveButton).toBeInTheDocument()
})
// Test case: should handle click actions correctly
test('should handle click actions correctly', async () => {
// Render the Heading component
render(<Heading />)
// Simulate clicking the "Cancel" button
await act(async () => {
const cancelButton = screen.getByRole('button', {
name: 'Cancel',
})
userEvent.click(cancelButton)
})
// Verify that onClose function is called
expect(onCloseSpy).toHaveBeenCalledTimes(1)
// Simulate clicking the "Save" button
await act(async () => {
const saveButton = screen.getByRole('button', {
name: 'Save',
})
userEvent.click(saveButton)
})
// Verify that onEdit function is called with expected parameter
expect(onEditSpy).toHaveBeenCalledTimes(1)
expect(onEditSpy).toHaveBeenCalledWith('FAKE_IMAGE_URL')
})
})
JavaScriptExample with Advanced Events (.tsx)
import ToolButton from './ToolButton'
import { expect, describe, test, vi } from 'vitest'
import { act, fireEvent, render, screen } from '@testing-library/react'
import '@testing-library/jest-dom/extend-expect'
import userEvent from '@testing-library/user-event'
// Test suite for the ToolButton component
describe('ToolButton component', () => {
// Test case: should render ToolButton component correctly
test('should render ToolButton component correctly', async () => {
// Render the ToolButton component with custom text
render(
<ToolButton>
<span>Test Button</span>
</ToolButton>
)
// Assert that the necessary elements are rendered
const toolButton = screen.getByRole('button', { name: 'Test Button' })
expect(toolButton).toBeInTheDocument()
})
// Test case: should execute press and click actions on ToolButton component correctly
test('should execute press and click actions correctly',
async () => {
let fakePressEventCallCount = 0
const fakeClickEvent = vi.fn()
const fakePressEvent = vi.fn(() => {
fakePressEventCallCount++
})
// Render the ToolButton component with custom text and custom events
render(
<ToolButton onClick={fakeClickEvent} onPress={fakePressEvent}>
<span>Test Button</span>
</ToolButton>
)
// Simulate clicking the "Test" button
await act(async () => {
const toolButton = screen.getByRole('button', { name: 'Test Button' })
userEvent.click(toolButton)
})
// Assert that the click event is called once
expect(fakeClickEvent).toBeCalledTimes(1)
// Simulate pressing and holding the "Test" button for 0.325 seconds
await act(async () => {
const toolButton = screen.getByRole('button', { name: 'Test Button' })
// Wait for 1 second to simulate holding the mouse press
fireEvent.mouseDown(toolButton)
await new Promise((resolve) => setTimeout(resolve, 1000)) // wait for 1 second
fireEvent.mouseUp(toolButton)
})
// The "Test" button should be pressed around 60 times within 1000ms.
// While accuracy is not critical for this test, it should be reasonably reliable.
// Therefore, setting the expectation to be greater than 30 to ensure test pass reliably.
expect(fakePressEventCallCount).toBeGreaterThan(30)
})
})
JavaScriptRunning Tests
In your package.json file, add these lines under the scripts section:
For watch mode: "test": "vitest"
For one-time execution mode: "test": "vitest --run""
References
React Testing Library Tutorial – How to Write Unit Tests for React Apps