The more your tests resemble the way your software is used the more confidence they can give you

Things we should avoid doing to test like the user

- Using class-based selectors to find elements

- Manually triggering the re-rendering of components

- Using fixed-duration waits when expecting state changes

- Simulating browser events

Using class-based selectors

// Instead of using
const usernameInput = wrapper.find('input').first();
const passwordInput = wrapper.find('input').last();

// ...or using
const usernameInput = wrapper.find('.sign-in-modal .username-field');
const passwordInput = wrapper.find('.sign-in-modal .password-field');

// We should consider
const usernameInput = getByLabelText('Username');
const passwordInput = getByLabelText('Password');

Using class-based selectors

react-testing-library  re-exports everything from dom-testing-library

// In order of preference
getByLabelText()

getByPlaceholderText()

getByText()

getByAltText()

getByTitle()

getByValue()

// Escape hatch
getByTestId()

All methods accept a TextMatch which can be either a string, regex or a predicate function

// The "for" attribute 
// (NOTE: in JSX with React you'll write "htmlFor" rather than "for")
<label for="username-input">Username</label>
<input id="username-input" />

// The aria-labelledby attribute
<label id="username-label">Username</label>
<input aria-labelledby="username-label" />

// Wrapper labels
<label>Username <input /></label>

// It will NOT find the input node for this:
<label><span>Username</span> <input /></label>

getByLabelText()

// A shortcut for document.querySelector(`[data-testid="${yourId}"]`)
// <input data-testid="username-input" />

const usernameInputElement = getByTestId(container, 'username-input')

getByTestId()

Should only be used in situations where all other selectors don't work for your use case.

// In a situation where you'd want to select a specific item in a list
const thirdLiInUl = container.querySelector('ul > li:nth-child(3)')


// You can include an index or ID in your attribute
const items = [
  /* your items */
]
const thirdItem = getByTestId(`item-${items[2].id}`)

Manually triggering the re-rendering of components

Nothing to do here! 😁

 

react-testing-library uses ReactDOM directly on JSDOM

JSDOM should update automatically as the browser would

Using fixed-duration waits when expecting state changes

// react-testing-library provides us with two wait methods:

// wait
// Polls the DOM with an assertion
// To be used for non-deterministic waits (API calls)
await wait(() => getByLabelText(container, 'username'))
getByLabelText(container, 'username').value = 'chucknorris'

// waitForElement
// Uses MutationObserver
// To be used for deterministic DOM changes
const usernameElement = await waitForElement(
  () => getByLabelText(container, 'username')
)

const [usernameElement, passwordElement] = waitForElement(
  () => [
    getByLabelText(container, 'username'),
    getByLabelText(container, 'password'),
  ]
)

Simulating browser events

// react-testing-library provides us with fireEvent

// <button>Submit</button>
const rightClick = {button: 2}
fireEvent.click(getElementByText('Submit'), rightClick)
// default `button` property for click events is set to `0` which is a left click.

Custom Jest Matchers

// toBeInTheDOM
expect(queryByTestId('count-value')).toBeInTheDOM()
expect(queryByTestId('count-value')).not.toBeInTheDOM()

// toHaveTextContent
expect(getByTestId('count-value')).toHaveTextContent('2')
expect(getByTestId('count-value')).not.toHaveTextContent('2')

// toBeVisible
expect(container.querySelector('header')).toBeVisible()
expect(container.querySelector('header')).not.toBeVisible()

jest-dom provides us with many DOM-specific Jest matchers

Test Structure

test('Fetch makes an API call and displays the greeting when load-greeting is clicked', async () => {
  // Arrange
  axiosMock.get.mockResolvedValueOnce({data: {greeting: 'hello there'}})
  const url = '/greeting'
  const {getByText, getByTestId, container} = render(App)

  // Act
  fireEvent.click(getByText('Load Greeting'))

  // let's wait for our mocked `get` request promise to resolve
  // wait will wait until the callback doesn't throw an error
  const greetingTextNode = await waitForElement(() =>
    getByTestId('greeting-text'),
  )

  // Assert
  expect(axiosMock.get).toHaveBeenCalledTimes(1)
  expect(axiosMock.get).toHaveBeenCalledWith(url)
  expect(getByTestId('greeting-text')).toHaveTextContent('hello there')
  expect(getByTestId('ok-button')).toHaveAttribute('disabled')
})

Used the "AAA" (Arrange-Act-Assert) testing pattern

Resources

https://github.com/kentcdodds/react-testing-library

https://github.com/gnapse/jest-dom

https://github.com/kentcdodds/dom-testing-library

react-testing-library

By Matan Kushner

react-testing-library

  • 667