Notes are based on this tutorial. Testing Library is built on to of Jest and gives api to test react UI. Install npm i -D @testing-library/react Component import React from 'react' import "./Header.css" export default function Header({ title }) { return ( <> <h1 title="Header" className="header">{title}</h1> <h3 data-testid="header-1" className="header">Hello</h3> </> ) } Queries import { render, screen } from '@testing-library/react' import App from './App' test('renders learn react link', () => { render(<App />) const linkElement = screen.getByText(/learn react/i) expect(linkElement).toBeInTheDocument() }) getBy... getByRole , getByLabelText , getByPlaceholderText getByText , getByDisplayValue , getByAltText getByTitle , getByTestId queryBy... queryByRole , queryByLabelText , queryByPlaceholderText , queryByText , queryByDisplayValue , queryByAltText queryByTitle , queryByTestId findBy... findByRole , findByLabelText , findByPlaceholderText findByText , findByDisplayValue , findByAltText findByTitle , findByTestId ..AllBy... All above methods have all companions like get All By which return results in array. Queries priorities getByRole --> getByLabelText --> getByPlaceholderText --> getByText --> getByDisplayValue --> getByAltText --> getByTitle --> getByTestId Use getByRole query as much as possible. Query examples getByRole HTML element roles can be checked here . it('should render same text passed into title prop', () => { render( <Header title="todo" /> ) const h1Element = screen.getByRole("heading") // works if we have only one heading expect(h1Element).toBeInTheDocument() }) Specify text inside element. it('should render same text passed into title prop', () => { render( <Header title="todo" /> ) const h1Element = screen.getByRole("heading", { name: /todo/i }) expect(h1Element).toBeInTheDocument() }) getByTitle it('should render same text passed into title prop', () => { render( <Header title="todo" /> ) const h1Element = screen.getByTitle("Header") expect(h1Element).toBeInTheDocument() }) getByTestId Not preferable query... it('should render same text passed into title prop', () => { render( <Header title="todo" /> ) const h2Element = screen.getByTestId("header-1") expect(h2Element).toBeInTheDocument() }) findByText Asynchronous query... it('should render same text passed into title prop', async () => { render( <Header title="todo" /> ) const h1Element = await screen.findByText(/todo/i) expect(h1Element).toBeInTheDocument() }) queryByText Matcher .not works with queryByText , because it doesn't fail when there is no match, opposite to getByText it('should render same text passed into title prop', () => { render( <Header title="todo" /> ) const h1Element = screen.queryByText(/dogs/i) expect(h1Element).not.toBeInTheDocument }) getAllByText Can specify number of expected elements... it('should render same text passed into title prop', () => { render( <Header title="todo" /> ) const h1Elements = screen.getAllByText(/todo/i) expect(h1Elements.length).toBe(1) }) within Can query element inside another one. import { fireEvent, screen, waitFor, within } from '@testing-library/react' it('3 checkboxes should render, active attachment checkbox should be disabled, others enabled', () => { renderWithProvider(<AttachmentsPopover />, {}, { preloadedState: store }) const openAttachmentButton = screen.getByTestId('open-attachments-button') fireEvent.click(openAttachmentButton) const popOver = screen.getByTestId('open-attachments-list') const checkboxes = within(popOver).getAllByRole('checkbox') expect(checkboxes.length).toBe(3) expect(checkboxes[0]).toHaveProperty('disabled', true) expect(checkboxes[1]).toHaveProperty('disabled', false) expect(checkboxes[2]).toHaveProperty('disabled', false) }) Render wrapped element Sometimes our components are wrapped inside library components and we need to bring it also to out tests. In example below we use Link from the react-router-dom import React from 'react' import "./TodoFooter.css" import { Link } from "react-router-dom" function TodoFooter({ numberOfIncompleteTasks }) { return ( <div className="todo-footer"> <p>{numberOfIncompleteTasks} {numberOfIncompleteTasks === 1 ? "task" : "tasks"} left</p> <Link to="/followers">Followers</Link> </div> ) } export default TodoFooter To test it we also need to bring it into the test. import { render, screen } from '@testing-library/react'; import TodoFooter from "../TodoFooter" import { BrowserRouter } from "react-router-dom" const MockTodoFooter = ({ numberOfIncompleteTasks }) => { return ( <BrowserRouter> <TodoFooter numberOfIncompleteTasks={numberOfIncompleteTasks} /> </BrowserRouter> ) } describe("TodoFooter", () => { it('should render the correct amount of incomplete tasks', () => { render( <MockTodoFooter numberOfIncompleteTasks={5} /> ) const pElement = screen.getByText(/5 tasks left/i) expect(pElement).toBeInTheDocument() }) }) Debug Show html of the screen in console. screen.debug() Assertions All assertion methods. toHaveBeenCalledTimes , toHaveBeenCalledWith , toHaveBeenLastCalledWith , toHaveBeenNthCalledWith , toHaveClass , toHaveDisplayValue , toHaveErrorMessage , toHaveFocus , toHaveFormValues , toHaveLastReturnedWith , toHaveLength , toHaveNthReturnedWith , toHaveProperty , toHaveReturned , toHaveReturnedTimes , toHaveReturnedWith , toHaveStyle , toHaveTextContent , toHaveValue , toMatch , toMatchInlineSnapshot , toMatchObject , toMatchSnapshot , toReturn , toReturnTimes , toReturnWith , toStrictEqual , toThrow , toThrowError , toThrowErrorMatchingInlineSnapshot , toThrowErrorMatchingSnapshot , lastCalledWith , lastReturnedWith , not , nthCalledWith , nthReturnedWith , rejects , resolves , toBe , toBeCalled , toBeCalledTimes , toBeCalledWith , toBeChecked , toBeCloseTo , toBeDefined , toBeDisabled , toBeEmptyDOMElement , toBeEnabled , toBeFalsy , toBeGreaterThan , toBeGreaterThanOrEqual , toBeInTheDocument , toBeInstanceof , toBeInvalid , toBeLessThan , toBeLessThanOrEqual , toBeNaN , toBeNull , toBePartiallyChecked , toBeRequired , toBeTruthy , toBeUndefined , toBeValid , toBeVisible , toContain , toContainElement , toContainEqual , toContainHTML , toEqual , toHaveAccessibleDescription , toHaveAccessibleName , toHaveAttribute , toHaveBeenCalled , toHaveBeenCalledTimes , toHaveBeenCalledWith , toHaveBeenLastCalledWith toBeInTheDocument describe("Header", () => { it.only('should render same text passed into title prop', () => { render(<Header title="todo" />) const h1Element = screen.getByText(/todo/i) expect(h1Element).toBeInTheDocument() }) }) toHaveTextContent it('should render correct text content', () => { render(<MockTodoFooter numberOfIncompleteTasks={1} />) const pElement = screen.getByText(/1 task left/i) expect(pElement).toHaveTextContent('1 task left') }) not it('should render correct text content', () => { render(<MockTodoFooter numberOfIncompleteTasks={1} />) const pElement = screen.getByText(/1 task left/i) expect(pElement).not.toHaveTextContent('2 task left') }) toBeVisible it('"task" should be visible when the number of incomplete tasks is one', () => { render( <MockTodoFooter numberOfIncompleteTasks={1} /> ) const pElement = screen.getByText(/1 task left/i) expect(pElement).toBeVisible() }) toContainHTML it('should contain p tag with correct text', () => { render(<MockTodoFooter numberOfIncompleteTasks={1} />) const pElement = screen.getByText(/1 task left/i) expect(pElement).toContainHTML('p') }) toBeFalsy it('should render correct text content', () => { render(<MockTodoFooter numberOfIncompleteTasks={1} />) const pElement = screen.getByText(/1 task left/i) expect(pElement).not.toBeFalsy() }) toBe Here we assert attribute value or text content. it('should render correct text content', () => { render(<MockTodoFooter numberOfIncompleteTasks={1} />) const pElement = screen.getByText(/1 task left/i) expect(pElement.textContent).toBe('1 task left') }) Fire event compositionEnd , compositionStart , compositionUpdate , keyDown , keyPress , keyUp focus , blur , focusIn , focusOut , submit , reset dragExit , dragLeave , dragOver , dragStart , drop , mouseDown , mouseEnter , mouseLeave , mouseMove , mouseOut , mouseOver , mouseUp select , touchCancel , touchEnd , touchMove , touchStart , resize , scroll wheel , abort , canPlay , canPlayThrough , durationChange , emptied , encrypted , ended , loadedData , loadedMetadata , loadStart , pause , play , playing , progress , rateChange , seeked , seeking , stalled , suspend , timeUpdate , volumeChange , waiting , load , error , animationStart , animationEnd , animationIteration , transitionCancel , transitionEnd , transitionRun , transitionStart , pointerOver , pointerEnter , pointerDown , pointerMove , pointerUp , pointerCancel , pointerOut , pointerLeave , gotPointerCapture , lostPointerCapture popState Test if input renders const mockedSetTodo = jest.fn() it('should render input element', () => { render(<AddInput todos={[]} setTodos={mockedSetTodo} />) const inputElement = screen.getByPlaceholderText(/Add a new task here.../i) expect(inputElement).toBeInTheDocument() }) Test if input value updates it('should be able to type into input', () => { render(<AddInput todos={[]} setTodos={mockedSetTodo} />) const inputElement = screen.getByPlaceholderText(/Add a new task here.../i) fireEvent.click(inputElement) fireEvent.change(inputElement, { target: { value: 'Go Grocery Shopping' } }) expect(inputElement.value).toBe('Go Grocery Shopping') }) Test if button click triggers a function it('should be able to type into input', () => { render(<AddInput todos={[]} setTodos={mockedSetTodo} />) const inputElement = screen.getByPlaceholderText(/Add a new task here.../i) fireEvent.click(inputElement) fireEvent.change(inputElement, { target: { value: 'Go Grocery Shopping' } }) const buttonElement = screen.getByRole('button', { name: /Add/i }) fireEvent.click(buttonElement) expect(mockedSetTodo).toBeCalled() }) Integration test Test of 2 components interact between each other. const MockTodo = () => { return ( <BrowserRouter> <Todo/> </BrowserRouter> ) } const addTask = (tasks) => { const inputElement = screen.getByPlaceholderText(/Add a new task here.../i) const buttonElement = screen.getByRole("button", { name: /Add/i} ) tasks.forEach((task) => { fireEvent.change(inputElement, { target: { value: task } }) fireEvent.click(buttonElement) }) } it('should add multiple input items into the list', () => { render( <MockTodo /> ) addTask(["Go Grocery Shopping", "Go Grocery Shopping", "Go Grocery Shopping"]) const divElements = screen.queryAllByText(/Go Grocery Shopping/i) expect(divElements.length).toBe(3) }) it('task should mark task as completed when clicked', () => { render( <MockTodo /> ) addTask(["Go Grocery Shopping"]) const divElement = screen.getByText(/Go Grocery Shopping/i) fireEvent.click(divElement) expect(divElement).toHaveClass("todo-item-active") }) Asynchronous test Component gets data in useEffect asynchronously. import React, { useEffect, useState } from 'react' import './FollowersList.css' import axios from 'axios' import { Link } from 'react-router-dom' export default function FollowersList() { const [followers, setFollowers] = useState([]) useEffect(() => { const fetchFollowers = async () => { const { data } = await axios.get('https://randomuser.me/api/?results=5') setFollowers(data.results) } fetchFollowers() }, []) return ( <div className="followerslist-container"> <div> {followers.map((follower, index) => ( <div className="follower-item" data-testid={`follower-item-${index}`}> <img src={follower.picture.large} /> <div className="followers-details"> <div className="follower-item-name"> <h4>{follower.name.first}</h4> <h4>{follower.name.last}</h4> </div> <p>{follower.login.username}</p> </div> </div> ))} </div> <div className="todo-footer"> <Link to="/">Go Back</Link> </div> </div> ) } To test the existence of first follower item we need to use async method findByTestId() . import { render, screen, fireEvent } from '@testing-library/react' import { BrowserRouter } from 'react-router-dom' import FollowersList from '../FollowersList' const MockFollowersList = () => { return ( <BrowserRouter> <FollowersList /> </BrowserRouter> ) } describe('FollowersList', () => { it('should render follower item', async () => { render(<MockFollowersList />) const followerDivElement = await screen.findByTestId('follower-item-0') expect(followerDivElement).toBeInTheDocument() }) }) Mock request It is not good idea to make tests with real http requests, because it may cost money, slow and dependent on 3rd party API. In unit tests we test our functions and ui components in isolation. Let's mock API response from axios call from previous example. Add later ... Before & After describe('FollowersList', () => { beforeEach(() => console.log('RUNS BEFORE EACH TEST')) beforeAll(() => console.log('RUNS ONCE BEFORE ALL TESTS')) afterEach(() => console.log('RUNS AFTER EACH TEST')) afterAll(() => console.log('RUNS ONCE AFTER ALL TESTS')) it('should run test', () => console.log('test')) it('should run test', () => console.log('test')) it('should run test', () => console.log('test')) }) Redux To test a component that uses redux we may provide custom render function, which will have pre-configured store and can accept additional store values as a parameter. Custom render function // renderWithProvider.js import { render } from '@testing-library/react' import { configureStore } from '@reduxjs/toolkit' import { Provider } from 'react-redux' import { ThemeProvider } from '@mui/styles' import themes from '../themes' import rootReducer from '../store/reducers' import { history } from '../store' export const renderWithProvider = ( ui, reducers, { preloadedState, store = configureStore({ reducer: rootReducer(history), preloadedState }), ...renderOptions } = {} ) => { const Wrapper = ({ children }) => ( <ThemeProvider theme={themes}> <Provider store={store}> {children} </Provider> </ThemeProvider> ) return render(ui, { wrapper: Wrapper, ...renderOptions }) } // App.test.js import { App } from './App' import { renderWithProvider } from '../../testUtils/renderWithProvider' import { screen } from '@testing-library/react' describe('App root component', () => { it('should render loader', () => { const store = { app: { isLoading: true, locale: 'fi-FI' } } renderWithProvider(<App routes={<div>I am route</div>} />, {}, { preloadedState: store }) expect(screen.getByRole('progressbar', { hidden: true })).toBeInTheDocument() }) it('should load "I am route"', () => { const store = { app: { isLoading: false, locale: 'fi-FI' }, user: { authorities: [] } } renderWithProvider(<App routes={<div>I am route</div>} />, {}, { preloadedState: store }) expect(screen.getByText(/I am route/i)).toBeInTheDocument() }) })