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

  • Pre-talk: Enzyme

  • What is react-testing-library?

  • What to avoid

  • Example scenarios using react-testing-library

  • jest-dom

  • Test structure

  • Cypress & cypress-testing-library

Agenda

Pre-Talk: Enzyme

Enzyme is powerful, but it inherently promotes bad testing practices, is not refactor-frienly and concentrates on implementation details

  1. Shallow rendering (avoidable with mount)
  2. Static rendering (like enzyme's render function).
  3. Pretty much most of enzyme's methods to query elements (like find) which include the ability to find by a component class or even its displayName
  4. Getting a component instance (like enzyme's instance)
  5. Getting and setting a component's props (props())
  6. Getting and setting a component's state (state())
1

Pre-Talk: Enzyme

With shallow rendering, I can refactor my component’s implementation and my tests break. With shallow rendering, I can break my application and my tests say everything’s still working.

 

 

react-testing-library

 

A simpler replacement for enzyme that encourages good testing and accessibility practices.

 

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}`)
/* Example.tsx */
const Example = () => (
  <div>
    {aFalseConditioal && <div>I don't render</div>}
  </div>

queryByText

to make an assertion that an element is not present in the DOM

/* Example.spec.ts */

// Testing that it doesn't exist. 
// We do not use getByText as get will throw an error 
// for a node that cannot be found
const { queryByText } = render(<Example/>);

// Note that /i ignores casing. You can use more than regex
expect(queryByText(/i don't render/i)).not.toBeInDocument();

// The assert below is the same, but is less declarative
// expect(queryByText(/I don't render/i)).toBeNull();

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

 

Enzyme only rerenders after simulating events and otherwise requires you to run the update() method to tell it to rerender.

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

Cypress

  • End to End testing
  • Never add waits or sleeps to your tests. Cypress automatically waits for commands and assertions before moving on
  • Debug directly from dev tools with readable stack traces
  • A lot more

cypress-testing-library

Adds additional commands, used to keep the same guiding principles

cy.getByTitle(
      "Fairmont Chicago at Millennium Park - Truncated title"
    ).click();

cy.getByLabelText()
cy.getByQueryText()
// ...

Resources

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

 

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

 

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

 

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

react-testing-library

By Tiffany Le-Nguyen

react-testing-library

A slightly modified version of https://slides.com/matchai/react-testing-library#/

  • 811