Notes
Short guide to React testing library
Tips and tricks for the common use cases with the @testing-library/react library

Testing can be intimidating especially for beginners like me years ago. I tried to avoid writing tests whenever I can to spare myself some mercy from dealing with the pain. However, I came to understand how tests works and eventually become very good at writing tests. Writing efficient tests can improve the software quality and confidence, which in turn improve user satisfaction and minimal unexpected behavior.

In this article, we will be taking a look on the testing library for React which literally called @testing-library/react that can be npm installed.

Simple test

Below is a sample test of how the code looks like. @testing-library/react uses Jest under the hood to perform tests.

import { render, screen } from '@testing-library/react'
import App from './App'

it('renders learn react link', () => {
  render(<App />)
  const linkElement = screen.getByText(/learn react/i)
  expect(linkElement).toBeInTheDocument()
})

Element selectors

There are two kinds of selector in this testing library, namely single target or multiple target selectors.

The selectors can also be grouped into 3 main categories. Each have different properties and usage which will be summarized in the table below.

getByfindByqueryBygetAllByfindAllByqueryAllBy
No matcherrorerrornullerrorerrorarray
1 matchreturnreturnreturnarrayarrayarray
1+ matcherrorerrorerrorarrayarrayarray
Awaitablenoyesnonoyesno

Query Accessibility

In theory when writing tests, we have to stay as close to user perspective to test the underlying behaviours without any blindspot that might have been missed while developing.

There are a plethora of query methods in React testing library and we won't be covering all of them here but if you are interested, please have a look on the official documentation.

Below are a few examples that are grouped according to their accessibility:

  • Accessible by everyone
    • getByRole
    • getByLabelText
    • getByPlaceholderText
    • getByText
  • Semantic queries (for screen reader)
    • getByAltText
    • getByTitle
  • Self-defined Test ID
    • getByTestId

Here we are using getBy for illustration and is also applicable for the findBy and queryBy variant as well.

The test id is a custom property created for elements that are impossible to obtained by any other query methods due to complexity and ambiguity. It is the last resort to query the elements and avoid using it if the elements is accessible by other methods.

It is recommended to use queries that are more close to user interaction and accessibility.

Unit tests

This section will illustrate the common use cases for unit testing with React testing library labelled by its scenario.

Passing props to a component

it('renders same text pass in props', async () => {
  render(<Header title="My Header" />)
  const headingElement = screen.getByText(/my header/i)
  expect(headingElement).toBeInTheDocument()
})

Get element by its role

it('renders same text pass in role', async () => {
  render(<Header title="My Header" />)
  const headingElement = screen.getByRole('heading')
  expect(headingElement).toBeInTheDocument()
})

Get element by role and text

Lets say there is 2 heading role element in the component. We can get specifically of one of them by specifying their name (which is their text)

it('renders same text pass in role', async () => {
  render(<Header title="My Header" />)
  const headingElement = screen.getByRole('heading', { name: 'My Header' })
  expect(headingElement).toBeInTheDocument()
})

By Test ID

// component under test
return <h1 data-testid="hello">Hello</h1>
it('renders same text pass in role', async () => {
  render(<Header title="My Header" />)
  const headingElement = screen.getByTestId('hello')
  expect(headingElement).toBeInTheDocument()
})

Using find by

As the operation is asynchronous, it needs await and async keyword to work.

it('renders same text pass in role', async () => {
  render(<Header title="My Header" />)
  const headingElement = await screen.findByTestId('hello')
  expect(headingElement).toBeInTheDocument()
})

Assert not

it('renders same text pass in props', async () => {
  render(<Header />)
  const headingElement = screen.getByText(/dogs/i)
  expect(headingElement).not.toBeInTheDocument()
})
// component
return <Link to="/link">Link</Link>
import { MemoryRouter } from 'react-router-dom'

const MockComponent = ({ someProp }) => (
  <MemoryRouter>
    <Component someProp={someProp} />
  </MemoryRouter>
)

it('should render with Link and prop', () => {
  render(<MockComponent someProp={4} />)
  const paragraphElement = screen.getByText(/fs/i)

  expect(paragraphElement).toBeInTheDocument()
})

Assert visible

// css opacity can affect this
expect(paragraphElement).toBeVisible()

To contain HTML tag

// to contain p tag
expect(paragraphElement).toContainHTML('p')

To have text content

expect(paragraphElement).toHaveTextContent('the text to assert')

More matchers can be found on Jest's documentation

Firing events

Triggering DOM events manually can be invaluable for testing as well as it mimic how the user interacts with the web application.

Check input

import { fireEvent } from '@testing-library/react'

const mockedSetTodo = jest.fn()

describe('AddInput', () => {
  it('should be able to type in input', async () => {
    render(<AddInput todos={[]} setTodos={mockedSetTodo} />)
    const inputElement = screen.getByPlaceholderText(/add a new task here.../i)

    fireEvent.change(inputElement, { target: { value: 'Go grocery shopping' } })

    expect(inputElement.value).toBe('Go grocery shopping')
  })
})

Reset input onclick

Sets the input to empty string when button is clicked.

it('should have empty input when add button is clicked', async () => {
  render(<AddInput todos={[]} setTodos={mockedSetTodo} />)

  const inputElement = screen.getByPlaceholderText(/add a new task here.../i)
  const buttonElement = screen.getByRole('button', { name: /Add/i })

  fireEvent.change(inputElement, { target: { value: 'Go grocery shopping' } })

  fireEvent.click(buttonElement)
  expect(inputElement.value).toBe('')
})

Integration test

Tests that involve 2 or more components.

it('should render same text passed into title prop', () => {
  render(<MockTodo />)
  const inputElement = screen.getByPlaceholderText(/add a new task here.../i)
  const buttonElement = screen.getByRole('button', { name: /Add/i })

  fireEvent.change(inputElement, { target: { value: 'Go grocery shopping' } })

  fireEvent.click(buttonElement)

  const divElement = screen.getByText(/Go grocery shopping/i)
  expect(divElement).toBeInTheDocument()
})
it('should render multiple elements', () => {
  render(<MockTodo />)
  const inputElement = screen.getByPlaceholderText(/add a new task here.../i)
  const buttonElement = screen.getByRole('button', { name: /Add/i })

  fireEvent.change(inputElement, { target: { value: 'Go grocery shopping' } })
  fireEvent.click(buttonElement)
  fireEvent.change(inputElement, { target: { value: 'Go grocery shopping' } })
  fireEvent.click(buttonElement)
  fireEvent.change(inputElement, { target: { value: 'Go grocery shopping' } })
  fireEvent.click(buttonElement)

  const divElements = screen.getAllByText(/Go grocery shopping/i)
  expect(divElements.length).toBe(3)
})
it('should not have completed class when initially rendered', () => {
  render(<MockTodo />)
  const inputElement = screen.getByPlaceholderText(/add a new task here.../i)
  const buttonElement = screen.getByRole('button', { name: /Add/i })

  fireEvent.change(inputElement, { target: { value: 'Go grocery shopping' } })

  fireEvent.click(buttonElement)

  const divElement = screen.getByText(/Go grocery shopping/i)
  expect(divElement).not.toHaveClass('todo-item-active')
})

Mocks

Mocks can be created in a __mock__ directory in root.

axios.js
const mockResponse = {
  data: {
    results: [
      // ...
    ],
  },
}

export default {
  get: jest.fn().mockResolvedValue(),
}

Conclusion

In this article, we've seen how the tests looks like using @testing-library/react to test a React application, differences between kinds and types of selectors available and a few example unit tests and integration tests.

Reference