Tiffany Le-Nguyen
Staff Frontend Engineer at Netlify ✈️
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
Enzyme is powerful, but it inherently promotes bad testing practices, is not refactor-frienly and concentrates on implementation details
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.
- 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
// 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');
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>
// A shortcut for document.querySelector(`[data-testid="${yourId}"]`)
// <input data-testid="username-input" />
const usernameInputElement = getByTestId(container, 'username-input')
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>
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();
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.
// 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'),
]
)
// 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.
// 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('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
Adds additional commands, used to keep the same guiding principles
cy.getByTitle(
"Fairmont Chicago at Millennium Park - Truncated title"
).click();
cy.getByLabelText()
cy.getByQueryText()
// ...
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
By Tiffany Le-Nguyen
A slightly modified version of https://slides.com/matchai/react-testing-library#/