nielsen-oss / docs

Guidelines & Best Practices documentation shared by Nielsen with the Open Source community
Apache License 2.0
40 stars 7 forks source link

Enzyme vs React Testing Library #3

Open einatnielsen opened 4 years ago

einatnielsen commented 4 years ago

The purpose of this issue is to review the pros and cons between Enzyme and React Testing Library.

RTL Pros:

  1. RTL is built on top of react-dom and react-dom/test-utils, thus supports new React features out of the box
  2. RTL approach is testing user behaviour which leads you to be more confident in your tests
  3. RTL limits the way you can interact with elements, and thus forces you to write code that is a11y compliant (best practice).
  4. FB recommend using RTL

Enzyme Cons:

  1. Enzyme is built on top of a custom 'react-dom', thus supporting React new features will not be out of the box
  2. Enzyme unit testing makes you lean towards testing implementation details (state and props) which your user does not care.
  3. Enzyme encourage using ID and css selector, by that your tests are likely to break when you refactor.
einatnielsen commented 4 years ago

I also found this tool called Spearmint: a tool to create RTL tests from UI, I tested it but I am not sure it is good enough: https://www.spearmintjs.com/

MatanBorenkraoutNielsen commented 4 years ago

One more thing I encountered: Testing components with async componentDidMount come in pretty complicated when trying to test them in enzyme: airbnb/enzyme#1587

RTL unlike Enzyme, waits and retries to get element until it is available therefor we can test async componentDidMount in other ways like loader appears and disappears and so on (many other ways available).

Another thing is like @einatnielsen mentioned, enzyme uses enzyme-adapter that also creates some extra configuration and from time to time - updates.

MatanBorenkraoutNielsen commented 4 years ago

Summary

CC: @lidoravitan @einatnielsen @liadShiranNielsen @thehulke @amitNielsen @MatanBorenkraoutNielsen

Issues:

With RTL:

function HiddenMessage({children}) {
  const [showMessage, setShowMessage] = React.useState(false)
  return (
    <div>
      <label htmlFor="toggle">Show Message</label>
      <input
        id="toggle"
        type="checkbox"
        onChange={e => setShowMessage(e.target.checked)}
        checked={showMessage}
      />
      {showMessage ? children : null}
    </div>
  )
}

test('shows the children when the checkbox is checked', () => {
  const testMessage = 'Test Message'
  const {queryByText, getByLabelText, getByText} = render(
    <HiddenMessage>{testMessage}</HiddenMessage>
  )

  expect(queryByText(testMessage)).toBeNull()
  fireEvent.click(getByLabelText(/show/i))
  expect(getByText(testMessage)).toBeInTheDocument()
})

With Enzyme:

function HiddenMessage({children}) {
  const [showMessage, setShowMessage] = React.useState(false)
  return (
    <div>
      <label htmlFor="toggle">Show Message</label>
      <input
        id="toggle"
        type="checkbox"
        class="my-checkbox-class"
        onChange={e => setShowMessage(e.target.checked)}
        checked={showMessage}
      />
      {showMessage ? <span class="my-children-class"> {children} </span> : null}
    </div>
  )
}

test('shows the children when the checkbox is checked', () => {
  const testMessage = 'Test Message'
  const wrapper = mount(
    <HiddenMessage>{testMessage}</HiddenMessage>
  )

  expect(wrapper.find('.my-children-class').text()).not.toBe('Test Message')

  const checkbox = wrapper.find('.my-checkbox-class')
  checkbox.simulate('change', { target: { checked: true } })

  expect(wrapper.find('.my-children-class').text()).toBe('Test Message')
})
  1. getByLabelText - find by label or aria-label text content
  2. getByPlaceholderText
  3. getByText
  4. getByDisplayValue
  5. getByAltText
  6. getByTitle
  7. getByRole
  8. getByTestId

With RTL:

 expect(getByText('Test Message')).toBeInTheDocument()

With Enzyme:

  const element = wrapper.find('.my-class')
  expect(element.text()).toBe('Test Message')

or just as seen in our Enzyme tests now:

mount(<MyComponent/>)
wait(randomNumberOfMs)

Waiting doesn't ensure us that what we wanted will happen, it's a matter of luck and our computer's CPU.

In RTL we can just:

import { render, wait } from "@testing-library/react";
const { getByText } = render(<MyComponent />);
await wait(() => expect(getByText("Mocked Data")).toBeInTheDocument());

@lidoravitan @einatnielsen - I didn't sum things up while in the meeting so if I missed something please let me know and I'll add it.

lidoravitan commented 4 years ago

@einatnielsen @MatanBorenkraoutNielsen what are the cons of using RTL? what are the risks of switching from enzyme? (@talJoffeExelate )

MatanBorenkraoutNielsen commented 4 years ago

At the moment we couldn't find the cons, as @thehulke mentioned, it looks like Enzyme was a step along the way and RTL actually expands its abilities and makes writing tests simpler and easier. The only thing we saw for now is that Enzyme is widely adopted and RTL is being adopted at these moments. Enzyme weekly downloads on npm are about 1.7M and RTL has only 500K. The main risk I see with switching is that we won't be able to rewrite all of our tests, and using both enzyme and RTL in our testbase might cause troubles with webapps-app-test-utils, looking forwards I believe this risk will pay out.

MatanBorenkraoutNielsen commented 4 years ago

@lidoravitan @einatnielsen While looking a bit more on RTL I found that they also have a cypress wrapper, it adds RTL functionality to cypress so selectors will work the same in Cypress as they will work when using RTL.

MatanBorenkraoutNielsen commented 4 years ago

CRA(Create React App) version 3.3.0 added built in support for React-Testing-Library: https://twitter.com/kentcdodds/status/1202591559272648705