Giter Site home page Giter Site logo

tsiq-swyx / react-testing-library Goto Github PK

View Code? Open in Web Editor NEW

This project forked from testing-library/react-testing-library

0.0 1.0 0.0 105 KB

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

Home Page: http://npm.im/react-testing-library

License: MIT License

JavaScript 100.00%

react-testing-library's Introduction

react-testing-library

goat

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' // see the tests for a full implementation

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 {getByText, getByTestId, container} = render(<Fetch url={url} />)

  // Act
  Simulate.click(getByText('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(getByTestId('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 === ''

getByLabelText(text: TextMatch, options: {selector: string = '*'}): HTMLElement

This will search for the label that matches the given TextMatch, then find the element associated with that label.

const inputNode = getByLabelText('Username')

// this would find the input node for the following DOM structures:
// 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>
//
// For this case, you can provide a `selector` in the options:
const inputNode = getByLabelText('username-input', {selector: 'input'})
// and that would work

Note: This method will throw an error if it cannot find the node. If you don't want this behavior (for example you wish to assert that it doesn't exist), then use queryByLabelText instead.

getByPlaceholderText(text: TextMatch): HTMLElement

This will search for all elements with a placeholder attribute and find one that matches the given TextMatch.

// <input placeholder="Username" />
const inputNode = getByPlaceholderText('Username')

NOTE: a placeholder is not a good substitute for a label so you should generally use getByLabelText instead.

getByText(text: TextMatch): HTMLElement

This will search for all elements that have a text node with textContent matching the given TextMatch.

// <a href="/about">About โ„น๏ธ</a>
const aboutAnchorNode = getByText('about')

getByTestId

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

// <input data-testid="username-input" />
const usernameInputElement = getByTestId('username-input')

In the spirit of the guiding principles, it is recommended to use this only after getByLabel, getByPlaceholderText or getByText don't work for your use case. Using data-testid attributes do not resemble how your software is used and should be avoided if possible. That said, they are way better than querying based on DOM structure. Learn more about data-testids from the blog post "Making your UI tests resilient to change"

TextMatch

Several APIs accept a TextMatch which can be a string, regex or a function which returns true for a match and false for a mismatch.

Here's an example

// <div>Hello World</div>
// all of the following will find the div
getByText('Hello World') // full match
getByText('llo worl') // substring match
getByText('hello world') // strings ignore case
getByText(/Hello W?oRlD/i) // regex
getByText((content, element) => content.startsWith('Hello')) // function

// all of the following will NOT find the div
getByText('Goodbye World') // non-string match
getByText(/hello world/) // case-sensitive regex with different case
// function looking for a span when it's actually a div
getByText((content, element) => {
  return element.tagName.toLowerCase() === 'span' && content.startsWith('Hello')
})

query APIs

Each of the get APIs listed in the render section above have a complimentary query API. The get APIs will throw errors if a proper node cannot be found. This is normally the desired effect. However, if you want to make an assertion that an element is not present in the DOM, then you can use the query API instead:

const submitButton = queryByText('submit')
expect(submitButton).toBeNull() // it doesn't exist

Examples

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

Feel free to contribute more!

FAQ

Which get method should I use?

Based on the Guiding Principles, your test should resemble how your code (component, page, etc.) as much as possible. With this in mind, we recommend this order of priority:

  1. getByLabelText: Only really good for form fields, but this is the number 1 method a user finds those elements, so it should be your top preference.
  2. getByPlaceholderText: A placeholder is not a substitute for a label. But if that's all you have, then it's better than alternatives.
  3. getByText: Not useful for forms, but this is the number 1 method a user finds other elements (like buttons to click), so it should be your top preference for non-form elements.
  4. getByTestId: The user cannot see (or hear) these, so this is only recommended for cases where you can't match by text or it doesn't make sense (the text is dynamic).

Other than that, you can also use the container to query the rendered component as well (using the regular querySelector API).

Can I write unit tests with this library?

Definitely yes! You can write unit and integration tests with this library. See below for more on how to mock dependencies (because this library intentionally does NOT support shallow rendering) if you want to unit test a high level component. The tests in this project show several examples of unit testing with this library.

As you write your tests, keep in mind:

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

What if my app is localized and I don't have access to the text in test?

This is fairly common. Our first bit of advice is to try to get the default text used in your tests. That will make everything much easier (more than just using this utility). If that's not possible, then you're probably best to just stick with data-testids (which is not bad anyway).

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, getByTestId} = render(<NumberDisplay number={1} />)
expect(getByTestId('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(getByTestId('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 {getByTestId, queryByTestId} = render(
    <HiddenMessage initialShow={true} />,
  )
  expect(queryByTestId('hidden-message')).toBeTruthy() // we just care it exists
  // hide the message
  Simulate.click(getByTestId('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')).toBeNull() // 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?"

What if I want to verify that an element does NOT exist?

You typically will get access to rendered elements using the getByTestId utility. However, that function will throw an error if the element isn't found. If you want to specifically test for the absence of an element, then you should use the queryByTestId utility which will return the element if found or null if not.

expect(queryByTestId('thing-that-does-not-exist')).toBeNull()
I really don't like data-testids, but none of the other queries make sense. Do I have to use a data-testid?

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 getByTestId utility:

const items = [
  /* your items */
]
const {getByTestId} = render(/* your component with the items */)
const thirdItem = getByTestId(`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 more your tests resemble the way your software is used, the more 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.

How does flushPromises work and why would I need it?

As mentioned before, flushPromises uses setImmediate to schedule resolving a promise after any pending tasks in the message queue are processed. This includes any promises fired before in your test.

If there are promise callbacks already in JavaScript's message queue pending to be processed at the time flushPromises is called, then these will be processed before the promise returned by flushPromises is resolved. So when you await flushPromises() the code immediately after it is guaranteed to occur after all the side effects of your async requests have ocurred. This includes any data your test components might have requested.

This is useful for instance, if your components perform any data requests and update their state with the results when the request is resolved. It's important to note that this is only effective if you've mocked out your async requests to resolve immediately (like the axios mock we have in the examples). It will not await for promises that are not already resolved by the time you attempt to flush them.

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 more your tests resemble the way your software is used, the more 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

๐Ÿ’ป

Paweล‚ Mikoล‚ajczyk

๐Ÿ’ป

Alejandro ร‘รกรฑez Ortiz

๐Ÿ“–

Matt Parrish

๐Ÿ› ๐Ÿ’ป ๐Ÿ“– โš ๏ธ

Justin Hall

๐Ÿ“ฆ

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

LICENSE

MIT

react-testing-library's People

Contributors

alejandronanez avatar audiolion avatar dnlsandiego avatar gnapse avatar miklet avatar pbomb avatar wkovacs64 avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.