1

React Component Testing Guide: Jest and RTL

 3 years ago
source link: https://hackernoon.com/react-component-testing-guide-jest-and-rtl-rx2533uz
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

React Component Testing Guide: Jest and RTL

@jaydeeJake Dowie

Jake is an experienced tech professional and founder of JDLT

Testing React components gives you confidence the component will work when the user interacts with it. As a junior full-stack developer on my first job, I found it extremely useful in helping me understand our current codebase as well as allowing me to add value while learning.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

This article is a summary of the information I found useful during my research and the answer to some challenges I came across. I don't hope to re-invent the wheel but to help others in a similar stage of their career. It's also assumed that you have some experience in writing tests.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Why Jest and RTL (React Testing Library)?

React openly recommends Jest as a test runner (perhaps because they maintain it) and RTL as their testing utility of choice. Jest is very fast, easy to set up and it has many powerful features such as mocking functions which allow you to replace a specific function and return a desirable value or to check how the test subject it’s executing the function. RTL is very simple to set up, easy to make queries (including asynchronously) and because of how it was built, it will help you write good tests.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Jest-Dom is not required but makes writing tests much easier because it will extend Jest matchers (methods that let you test values in different ways (e.g. 

toBe()
toHaveBeenCalled()
), and allow you to write clearer tests.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Another popular tool is Enzyme, but many believe that it can lead to bad testing practices. The main concern is that Enzyme offers extra utilities that allow you to test the internal workings of a component (e.g. read and set state of the component). The team at React tests React; therefore, there is no need for you to test React’s functionality such as state,

componentDidMount
, etc. The same goes for other libraries you may use.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

What to test?

When testing React components, the focus should be on replicating how the user would interact with the React component. This means that we should test for what the user should see or not see, and how they are meant to interact with the app once it renders (e.g. the value of search/input field can be changed) instead of testing implementation (e.g. was

componentDidMount
called x number of times).
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Some good questions to ask yourself when writing tests are:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  • What does the component render? Also, does it render differently under different conditions?

This is what the user will see and potentially interact with. By thinking about it, you will also realise that users should access and see different information depending on certain conditions being met

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  • What happens when the user interacts with the component?

These are the parts of the app which the user will click, write into, etc. and they’ll expect something to happen. Whatever is meant to happen, needs to be tested that happens when the event is triggered!

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  • When a function is passed in as a prop, how does the component use it?

You may need to recreate the behaviour of this function by using Jest’s mocking concept to know if the function has been called and the correct values were used

0 reactions
heart.png
light.png
money.png
thumbs-down.png

How to write a test?

RTL’s most used functions are 

render
 – renders the component,
cleanup
 – unmounts React DOM tree that were mounted with
render
and 
fireEvent
 – to fire events like a click.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Jest most used functions are 

expect
 along with a matcher. 
jest.fn()
will allow you to mock a function directly, 
jest.spyOn()
 to mock an object method and 
jest.mock()
 for an entire module.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The test should be structured as follows:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  1. Declare all 
    jest.fn()
    /
    spyOn()
    /
    mock()
     with or without mocked implementations
  2. Call RTL’s 
    render
    function with the test subject as an argument – provide context whenever the component consumes a context. Also, if React-Router Link is used in this component, an object with a property wrapper and value MemoryRouter (imported from React-Router) must be passed as the second argument. Optionally wrap the component in MemoryRouter tags
  3. Query the React DOM tree by using RTL’s query functions (e.g.
    getByRole()
    ) and check the values by call
  4. Check values queried by calling 
    expect()
     along with the relevant matcher. To replicate user interaction use 
    fireEvent

RTL also returns a 

debug()
 method when render is called. Debug is fantastic for checking what is rendered in the React tree for situations like debugging your tests.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

We will use the code below (a search field) as our example of a React component:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
render = () => {
  const {
    validateSelection,
    minCharacters,
    placeholder,
    inputFluid,
    inputLabel,
    clear
  }: any = this.props

  const { isLoading, value, results } = this.state

  const icon = validateSelection ? (
    <Icon name="check" color="green" />
  ) : (
    <Icon name="search" />
  )

  return (
    <Search
      minCharacters={minCharacters}
      loading={isLoading}
      icon={icon}
      onResultSelect={this.onResultSelect}
      onSearchChange={this.onSearchChange}
      results={results}
      value={clear ? null : value}
      fluid
      placeholder={placeholder}
      input={{ fluid: inputFluid, label: inputLabel }}
    />
  )
}

Above we are destructuring props and state. We are also returning a Semantic UI React 

Search
 module. In essence, the above will render an input field. When changed, it will call 
onSearchChange
 and Semantic UI React will automatically pass two arguments, 
event
 and 
data
 (all props, including current value). One of 
onSearchChange
’s jobs is to call an API and return results that match the current value.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Below are the tests we built for this component.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
import '@testing-library/jest-dom/extend-expect'
import React from 'react'
import { render, cleanup, fireEvent } from '@testing-library/react'
import SearchField from './SearchField'

afterEach(cleanup)
jest.useFakeTimers()

test('<SearchField />', () => {
  const handleResultSelectMock = jest.fn()
  const apiServiceMock = jest
    .fn()
    .mockImplementation(() =>
      Promise.resolve({ entity: { success: true, data: ['hello', 'adios'] } })
    )

  const { getByRole, debug } = render(
    <SearchField
      handleResultSelect={handleResultSelectMock}
      apiService={apiServiceMock}
    />
  )

  const input = getByRole('textbox')
  expect(apiServiceMock).not.toHaveBeenCalled()
  expect(input).toHaveValue('')

  fireEvent.change(input, { target: { value: 'search' } })
  expect(input).toHaveValue('search')
  jest.advanceTimersByTime(600)

  expect(apiServiceMock).toHaveBeenCalledWith('search')
  expect(apiServiceMock).toHaveBeenCalledTimes(1)
  debug()
})

What is happening in the example above?

0 reactions
heart.png
light.png
money.png
thumbs-down.png

We imported all dependencies needed to test this component.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  • Jest DOM - to extend jest matchers
  • render
    cleanup
    fireEvent
     - React Testing Library utilities
  • SearchField
     - the React component being tested
import '@testing-library/jest-dom/extend-expect'
import React from 'react'
import { render, cleanup, fireEvent } from '@testing-library/react'
import SearchField from './SearchField'

We called Jest's function 

afterEach
 and passed RTL's method 
cleanup
as an argument. cleanup will make sure that there are no memory leaks between tests by unmounting everything mounted by RTL's 
render
method. We also called Jest's 
useFakeTimers
 hook to mock timer functions.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
afterEach(cleanup)
jest.useFakeTimers()

The component requires two props which should be functions. Therefore, we started by mocking two functions that will be passed to the component as props -

handleResultSelectMock
 and 
apiServiceMock
.
handleResultSelectMock
 will be passed to 
handleResultSelect
 and
apiServiceMock
 to 
apiService
. Then, RTL's 
render
 method is called with SearchField component as the argument.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
test('<SearchField />', () => {
  const handleResultSelectMock = jest.fn()
  const apiServiceMock = jest
    .fn()
    .mockImplementation(() =>
      Promise.resolve({ entity: { success: true, data: ['hello', 'adios'] } })
    )

  const { getByRole, debug } = render(
    <SearchField
      handleResultSelect={handleResultSelectMock}
      apiService={apiServiceMock}
    />
  )
})

There will be times when the component being tested will require a

wrapper: Memory Router
 or a 
context
 to render successfully. See the example below:
0 reactions
heart.png
light.png
money.png
thumbs-down.png
const { getByTestId, container } = render(
  <UserContext.Provider value={context}>
    <MainLoggedIn
      config={{
        get: jest.fn().mockImplementation(() => ({
          globalMenu: [{ requiredPermissions: ['Navbar'] }]
        }))
      }}
      history={{ history: ['first_history', 'second_history'] }}
      children={['first_child', 'second_child']}
    />
  </UserContext.Provider>,
  { wrapper: MemoryRouter }
)

After 

render
 is called, we should query the React DOM tree and find the elements we want to test. Below we used 
getByRole
, but RTL offers many other query selectors functions.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
const input = getByRole('textbox')

To check values, start with the function 

expect
 along one of the several matchers. Here we started by checking that the apiServiceMock has not been called, then checks that the input field is empty (
value = ''
) when the component first renders.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
expect(apiServiceMock).not.toHaveBeenCalled()
expect(input).toHaveValue('')

An event is fired using the function 

change
 of RTL's 
fireEvent
 to replicate the user's behaviour. This event will update the value of the input field from 
''
 to 
'search'
. You can replicate other scenarios by using other 
fireEvent
 methods such as 
click()
mouseOver()
. Jest's
advanceTimersByTime
 method is called to move the mocked timer forward by 600ms hence the number 600 is passed as an argument.
advanceTimersByTime
 makes sure that tasks that have been queued by a timer function and would be executed within the given time (600ms in this case) will be executed.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
fireEvent.change(input, { target: { value: 'search' } })
expect(input).toHaveValue('search')
jest.advanceTimersByTime(600)

After firing the event, we expect a few things to happen, the

apiServiceMock
 function to be called once, and the argument passed to
apiServiceMock
 to match the current input's value.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
expect(apiServiceMock).toHaveBeenCalledWith('search')
expect(apiServiceMock).toHaveBeenCalledTimes(1)
debug()

Lastly, the 

debug
 function is called to check what is rendered in the React tree and help debug the tests.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Summary

Small and straightforward tests are better. Test each component independently. Focus on testing what the user will see and how they will interact with the component. Start building the tests after assessing what needs to be tested.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

More on the topic

Previously published at https://jdlt.co.uk/blog/testing-react-components-with-jest-and-react-testing-library/

0 reactions
heart.png
light.png
money.png
thumbs-down.png
5
heart.pngheart.pngheart.pngheart.png
light.pnglight.pnglight.pnglight.png
boat.pngboat.pngboat.pngboat.png
money.pngmoney.pngmoney.pngmoney.png
Share this story
Join Hacker Noon

Create your free account to unlock your custom reading experience.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK