4 minute read

React Testing Library is a popular testing library that allows developers to write tests for React components. It aims to provide a way to test components in a way that more closely resembles how they are used by real users.

Resources

Set Up

First, set up Jest for TypeScript.

  • Add jest-dom package for additional test helper functions

  • import '@testing-library/jest-dom/extend-expect' to use utility functions like toBeInTheDocument()

yarn add -D @testing-library/jest-dom

Then add an import to your jest setup file

// jest.config.js
module.exports = {
  clearMocks: true,
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coveragePathIgnorePatterns: ['/node_modules/'],
  coverageProvider: 'v8',
  preset: 'ts-jest',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: 'jsdom',
  transform: {
    '^.+\\.(ts|tsx)?$': 'ts-jest',
    '^.+\\.(js|jsx)$': 'babel-jest',
  },
}
// jest.setup.js
import '@testing-library/jest-dom/extend-expect'

An example test is like this

import * as React from 'react'
import { render, screen } from '@testing-library/react'
import List from '../src/components/List'
import '@testing-library/jest-dom/extend-expect'

describe('List', () => {
  const milk = 'Milk'
  const eggs = 'Eggs'
  const items = [
    { name: milk, done: false },
    { name: eggs, done: true },
  ]

  it('renders a list of items', () => {
    render(<List items={items} toggle={jest.fn()} />)

    expect(screen.getByText(milk)).toBeTruthy()
    expect(screen.getByRole('checkbox', { name: milk })).not.toBeChecked()

    expect(screen.getByText(eggs)).toBeTruthy()
  })
})

User Interactions

user-event is a companion library for Testing Library that simulates user interactions by dispatching the events that would happen if the interaction took place in a browser. Take note that all the functions return promise so use it with await

yarn add -D @testing-library/user-event
  • type

  • click

import * as React from 'react'
import { render, screen } from "@testing-library/react"
import Add from '../src/components/Add'
import userEvent from '@testing-library/user-event'

describe('Add', () => {
  const user = userEvent.setup()
  const addToItems = jest.fn()
  const item = 'Milk'

  it('calls addToItems when the button is clicked', async () => {
    render(<Add addToItems={addToItems} />)

    await user.type(screen.getByRole('textbox'), item)
    await user.click(screen.getByRole('button'))

    expect(addToItems).toHaveBeenCalledWith({ name: item, done: false })
  })
})
await userEvent.click(screen.getByRole('radio', { name: 'Yes' }));
await userEvent.selectOptions(
  screen.getByRole('combobox'),
  'Indication or potential indication of significant harm'
);
await userEvent.type(
  screen.getByRole('textbox', { name: 'Agreed action' }),
  agreedActionDescription
);
await userEvent.click(screen.getByRole('checkbox'));
await userEvent.click(screen.getByRole('button', { name: /next/i }));

Selectors

getByRole

  • textbox: input, textarea

  • checkbox: checkbox

  • radio: radio button

  • button: button

  • combobox: <select />

One thing to note is name doesn’t mean the name attribute in the element. It’s the text in the associated label.

// select a button with the label
const submitButton = screen.getByRole('button', { name: /ACTION/i })

it('save button should be enabled when all required inputs are done', async () => {
  await act(async () => {
    renderWithKompass(<AdminNoteForm formMode={FORM.CREATE} onSubmit={jest.fn()} />);
    await userEvent.type(
      screen.getByRole('textbox', { name: 'Note description' }),
      'Note description'
    );

    expect(screen.getByRole('button', { name: 'SAVE' })).toHaveProperty('disabled', false);
  });
});
expect(screen.getByRole('textbox', { name: 'Concern' })).toHaveValue('Concern description')
expect(screen.getByRole('textbox', { name: 'Agreed action' })).toHaveValue(
  'Agreed action description'
);
expect(screen.getByRole('radio', { name: 'Yes' })).toBeChecked();
expect(screen.getByRole('combobox', { name: 'Primary reason for review' })).toHaveValue(
  'Indication or potential indication of significant harm'
);

Assertions

toBeDefined()

Ensure that a variable is not undefined

it('should render the label text', () => {
  renderWithTheme(<ConfirmationBox {...props} />);
  expect(screen.getByText('I confirm that I have followed the')).toBeDefined();
});

toBeInTheDocument()

Check if an element is in the document

import * as React from 'react'
import { render, screen } from "@testing-library/react"
import Add from '../src/components/Add'
import userEvent from '@testing-library/user-event'

describe('Add', () => {
  const user = userEvent.setup()
  const addToItems = jest.fn()
  const item = 'Milk'

  it('renders an input and a button', () => {
    render(<Add addToItems={addToItems} />)

    expect(screen.getByRole('textbox')).toBeInTheDocument()
    expect(screen.getByRole('button')).toBeInTheDocument()
  })

  it('displays an error message when the item is empty', async () => {
    render(<Add addToItems={addToItems} />)

    await user.click(screen.getByRole('button'))

    expect(screen.getByText('The item cannot be empty.')).toBeInTheDocument()
  })
})

toHaveBeenCalledWith()

import * as React from 'react'
import { render, screen } from "@testing-library/react"
import Add from '../src/components/Add'
import userEvent from '@testing-library/user-event'

describe('Add', () => {
  const user = userEvent.setup()
  const addToItems = jest.fn()
  const item = 'Milk'

  it('calls addToItems when the button is clicked', async () => {
    render(<Add addToItems={addToItems} />)

    await user.type(screen.getByRole('textbox'), item)
    await user.click(screen.getByRole('button'))

    expect(addToItems).toHaveBeenCalledWith({ name: item, done: false })
  })

  it('does not call addToItems when the button is clicked but the item is empty', async () => {
    render(<Add addToItems={addToItems} />)

    await user.click(screen.getByRole('button'))

    expect(addToItems).not.toHaveBeenCalled()
  })
})

toBeChecked()

When a radio button is selected

<input 
  type="radio" 
  name="requireReview" 
  value="true"
>
  <label>Yes</label>
</input>

expect(screen.getByRole('radio', { name: 'Yes' })).toBeChecked();

waitFor()

Wait for a while to evaluate the expression

await waitFor(() => expect(screen.getByRole('textbox', { name: 'Concern' }))
				.toHaveValue('Concern description')
	      );

Gotchas

react-script test is not finding any test

The react-scripts test command is used to run tests in a React project created with create-react-app. If you are running this command and it is not finding any tests, there could be a few possible reasons:

  • Test files are not named correctly: By default, create-react-app looks for files with the .test.(j|t)s or .spec.(j|t)s file extension to run tests. Make sure your test files are named appropriately.

  • Test files are not in the correct directory: By default, create-react-app looks for test files in the src directory. Make sure your test files are located in the src directory or a subdirectory of src.

The input element doesn’t have the new value

Make sure you find the element again to check the new value. Don’t use a variable that holds the element as the value of the input is not updated.

it('clears the input when the button is clicked', async () => {
    render(
      <MockedProvider mocks={mocks} addTypename={false}>
        <Add />
      </MockedProvider>
    )

    await user.type(screen.getByRole('textbox'), item)
    await user.click(screen.getByRole('button'))

    await waitFor(() => expect(screen.getByRole('textbox')).toHaveValue(''))
		// do screen.getByRole('textbox') again.
  })

Comments