github

kentcdodds / react-testing-library

  • среда, 21 марта 2018 г. в 00:20:08
https://github.com/kentcdodds/react-testing-library

JavaScript
Simple and complete React DOM testing utilities that encourage good testing practices.



react-testing-library

Simple and complete React DOM testing utilities that encourage good testing practices.


Build Status Code Coverage version downloads MIT License

All Contributors PRs Welcome Code of Conduct

Watch on GitHub Star on GitHub Tweet

The problem

You want to write maintainable tests for your React components. However, the de facto standard for testing (enzyme) is bloated with complexity and features, most of which encourage poor testing practices (mostly relating to testing implementation details).

This solution

The react-testing-library is a very light-weight solution for testing React components. It provides light utility functions on top of react-dom and react-dom/test-utils, in a way that encourages better testing practices.

Table of Contents

Installation

This module is distributed via npm which is bundled with node and should be installed as one of your project's devDependencies:

npm install --save-dev react-testing-library

This library has a peerDependencies listing for react-dom.

Usage

// __tests__/fetch.js
import React from 'react'
import {render, Simulate, flushPromises} from 'react-testing-library'
import axiosMock from 'axios'
import Fetch from '../fetch'

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

  // Act
  Simulate.click(queryByTestId('load-greeting'))

  // let's wait for our mocked `get` request promise to resolve
  await flushPromises()

  // Assert
  expect(axiosMock.get).toHaveBeenCalledTimes(1)
  expect(axiosMock.get).toHaveBeenCalledWith(url)
  expect(queryByTestId('greeting-text').textContent).toBe('hello there')
  expect(container.firstChild).toMatchSnapshot()
})

Simulate

This is simply a re-export from the Simulate utility from react-dom/test-utils. See the docs.

flushPromises

This is a simple utility that's useful for when your component is doing some async work that you've mocked out, but you still need to wait until the next tick of the event loop before you can continue your assertions. It simply returns a promise that resolves in a setImmediate. Especially useful when you make your test function an async function and use await flushPromises().

See an example in the section about render below.

render

In the example above, the render method returns an object that has a few properties:

container

The containing DOM node of your rendered React Element (rendered using ReactDOM.render). It's a div. This is a regular DOM node, so you can call container.querySelector etc. to inspect the children.

Tip: To get the root element of your rendered element, use container.firstChild.

unmount

This will cause the rendered component to be unmounted. This is useful for testing what happens when your component is removed from the page (like testing that you don't leave event handlers hanging around causing memory leaks).

This method is a pretty small abstraction over ReactDOM.unmountComponentAtNode

const {container, unmount} = render(<Login />)
unmount()
// your component has been unmounted and now: container.innerHTML === ''

queryByTestId

A shortcut to container.querySelector(`[data-testid="${yourId}"]`). Read more about data-testids below.

const usernameInputElement = queryByTestId('username-input')

More on data-testids

The queryByTestId utility is referring to the practice of using data-testid attributes to identify individual elements in your rendered component. This is one of the practices this library is intended to encourage.

Learn more about this practice in the blog post: "Making your UI tests resilient to change"

Examples

You'll find examples of testing with different libraries in the test directory. Some included are:

Feel free to contribute more!

FAQ

How do I update the props of a rendered component?

It'd probably be better if you test the component that's doing the prop updating to ensure that the props are being updated correctly (see the Guiding Principles section). That said, if you'd prefer to update the props of a rendered component in your test, the easiest way to do that is:

const {container, queryByTestId} = render(<NumberDisplay number={1} />)
expect(queryByTestId('number-display').textContent).toBe('1')

// re-render the same component with different props
// but pass the same container in the options argument.
// which will cause a re-render of the same instance (normal React behavior).
render(<NumberDisplay number={2} />, {container})
expect(queryByTestId('number-display').textContent).toBe('2')

Open the tests for a full example of this.

If I can't use shallow rendering, how do I mock out components in tests?

In general, you should avoid mocking out components (see the Guiding Principles section). However if you need to, then it's pretty trivial using Jest's mocking feature. One case that I've found mocking to be especially useful is for animation libraries. I don't want my tests to wait for animations to end.

jest.mock('react-transition-group', () => {
  const FakeTransition = jest.fn(({children}) => children)
  const FakeCSSTransition = jest.fn(
    props =>
      props.in ? <FakeTransition>{props.children}</FakeTransition> : null,
  )
  return {CSSTransition: FakeCSSTransition, Transition: FakeTransition}
})

test('you can mock things with jest.mock', () => {
  const {queryByTestId} = render(<HiddenMessage initialShow={true} />)
  expect(queryByTestId('hidden-message')).toBeTruthy() // we just care it exists
  // hide the message
  Simulate.click(queryByTestId('toggle-message'))
  // in the real world, the CSSTransition component would take some time
  // before finishing the animation which would actually hide the message.
  // So we've mocked it out for our tests to make it happen instantly
  expect(queryByTestId('hidden-message')).toBeFalsy() // we just care it doesn't exist
})

Note that because they're Jest mock functions (jest.fn()), you could also make assertions on those as well if you wanted.

Open full test for the full example.

This looks like more work that shallow rendering (and it is), but it gives you more confidence so long as your mock resembles the thing you're mocking closly enough.

If you want to make things more like shallow rendering, then you could do something more like this.

Learn more about how Jest mocks work from my blog post: "But really, what is a JavaScript mock?"

I don't want to use data-testid attributes for everything. Do I have to?

Definitely not. That said, a common reason people don't like the data-testid attribute is they're concerned about shipping that to production. I'd suggest that you probably want some simple E2E tests that run in production on occasion to make certain that things are working smoothly. In that case the data-testid attributes will be very useful. Even if you don't run these in production, you may want to run some E2E tests that run on the same code you're about to ship to production. In that case, the data-testid attributes will be valuable there as well.

All that said, if you really don't want to ship data-testid attributes, then you can use this simple babel plugin to remove them.

If you don't want to use them at all, then you can simply use regular DOM methods and properties to query elements off your container.

const firstLiInDiv = container.querySelector('div li')
const allLisInDiv = container.querySelectorAll('div li')
const rootElement = container.firstChild

What if I’m iterating over a list of items that I want to put the data-testid="item" attribute on. How do I distinguish them from each other?

You can make your selector just choose the one you want by including :nth-child in the selector.

const thirdLiInUl = container.querySelector('ul > li:nth-child(3)')

Or you could include the index or an ID in your attribute:

<li data-testid={`item-${item.id}`}>{item.text}</li>

And then you could use the queryByTestId:

const items = [
  /* your items */
]
const {queryByTestId} = render(/* your component with the items */)
const thirdItem = queryByTestId(`item-${items[2].id}`)

What about enzyme is "bloated with complexity and features" and "encourage poor testing practices"

Most of the damaging features have to do with encouraging testing implementation details. Primarily, these are shallow rendering, APIs which allow selecting rendered elements by component constructors, and APIs which allow you to get and interact with component instances (and their state/properties) (most of enzyme's wrapper APIs allow this).

The guiding principle for this library is:

The less your tests resemble the way your software is used, the less confidence they can give you. - 17 Feb 2018

Because users can't directly interact with your app's component instances, assert on their internal state or what components they render, or call their internal methods, doing those things in your tests reduce the confidence they're able to give you.

That's not to say that there's never a use case for doing those things, so they should be possible to accomplish, just not the default and natural way to test react components.

Other Solutions

In preparing this project, I tweeted about it and Sune Simonsen took up the challenge. We had different ideas of what to include in the library, so I decided to create this one instead.

Guiding Principles

The less your tests resemble the way your software is used, the less confidence they can give you.

We try to only expose methods and utilities that encourage you to write tests that closely resemble how your react components are used.

Utilities are included in this project based on the following guiding principles:

  1. If it relates to rendering components, it deals with DOM nodes rather than component instances, nor should it encourage dealing with component instances.
  2. It should be generally useful for testing individual React components or full React applications. While this library is focused on react-dom, utilities could be included even if they don't directly relate to react-dom.
  3. Utility implementations and APIs should be simple and flexible.

At the end of the day, what we want is for this library to be pretty light-weight, simple, and understandable.

Contributors

Thanks goes to these people (emoji key):


Kent C. Dodds

💻 📖 🚇 ⚠️

Ryan Castner

📖

Daniel Sandiego

💻

This project follows the all-contributors specification. Contributions of any kind welcome!

LICENSE

MIT