RTL
(react-testing-library)
React Testing Libraries
- ReactTestUtil/TestRenderer
- enzyme
- react-testing-library
Sample component
import React, { useState, useEffect } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `you clicked ${count} times`;
})
return (
<div>
<p data-testid="current-count">{count}</p>
<button onClick={() => setCount(count + 1)}>
<span>button</span>
</button>
</div>
)
}
export default Counter;
Testing with test-utils
it('counter increments the count with test-utils', () => {
// Test first render and effect
act(() => {
ReactDOM.render(<Counter />, container);
});
const button = container.querySelector('button');
const label = container.querySelector('p');
expect(label.textContent).toBe('0');
expect(document.title).toBe('you clicked 0 times');
// Test second render and effect
act(() => {
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(label.textContent).toBe('1');
expect(document.title).toBe('you clicked 1 times');
})
Testing with test-utils
PASS src/components/counter/__tests__/index.js
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.299s
Testing with test-utils
it('counter increments the count with test-utils', () => {
// Test first render and effect
// act(() => {
ReactDOM.render(<Counter />, container);
// });
const button = container.querySelector('button');
const label = container.querySelector('p');
expect(label.textContent).toBe('0');
expect(document.title).toBe('you clicked 0 times');
// Test second render and effect
act(() => {
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(label.textContent).toBe('1');
expect(document.title).toBe('you clicked 1 times');
})
Testing with test-utils
FAIL src/components/counter/__tests__/index.js
● counter increments the count with test-utils
expect(received).toBe(expected) // Object.is equality
Expected: "you clicked 0 times"
Received: "you clicked 1 times"
39 |
40 | expect(label.textContent).toBe('0');
> 41 | expect(document.title).toBe('you clicked 0 times');
| ^
42 |
43 | // Test second render and effect
44 | act(() => {
at Object.toBe (src/components/counter/__tests__/index.js:41:26)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Testing with enzyme
it('counter increments the count with enzyme mount', () => {
const component = mount(<Counter />);
expect(document.title).toBe('you clicked 0 times');
expect(component.find('p').text()).toBe('0')
act(() => {
component.find('button').simulate('click');
})
expect(document.title).toBe('you clicked 1 times');
expect(component.find('p').text()).toBe('1')
})
Testing with enzyme
PASS src/components/counter/__tests__/index.js
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.299s
Testing with enzyme
it('counter increments the count with enzyme mount', () => {
const component = mount(<Counter />);
expect(document.title).toBe('you clicked 0 times');
expect(component.find('p').text()).toBe('0')
// act(() => {
component.find('button').simulate('click');
// })
expect(document.title).toBe('you clicked 1 times');
expect(component.find('p').text()).toBe('1')
})
Testing with enzyme
FAIL src/components/counter/__tests__/index.js
● counter increments the count with enzyme mount
expect(received).toBe(expected) // Object.is equality
Expected: "you clicked 1 times"
Received: "you clicked 0 times"
70 | // })
71 |
> 72 | expect(document.title).toBe('you clicked 1 times');
| ^
73 |
74 | expect(component.find('p').text()).toBe('1')
75 | })
at Object.toBe (src/components/counter/__tests__/index.js:72:26)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Testing with RTL
it('counter increments the count with RTL', () => {
const component = render(<Counter />)
const button = component.getByText('button');
const currentCount = component.getByTestId('current-count');
expect(currentCount.textContent).toBe('0')
expect(document.title).toBe('you clicked 0 times')
fireEvent.click(button)
expect(currentCount.textContent).toBe('1')
expect(document.title).toBe('you clicked 1 times')
})
Testing with RTL
PASS src/components/counter/__tests__/index.js
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.299s
RTL
- User interaction
- No implementation details
- No shallow rendering
- dom-testing-library
- Kent C. Dodds
Kent C Dodds
Testing should be about acquiring confidence

testingjavascript.com
kentcdodds.com/blog
shallow vs mount
render (
<TodoInput
onChange={newTaskText => this.setState({ newTaskText })}
value={newTaskText}
onSubmit={() => {
this.setTasks([...tasks, { text: newTaskText, done: false }]);
this.setState({ newTaskText: "" });
}}
/>
<TodoList
tasks={tasks}
deleteTask={i => {
tasks.splice(i, 1);
this.setTasks([...tasks]);
}}
toggleTask={i => {
tasks[i].done = !tasks[i].done;
this.setTasks([...tasks]);
}}
/>
)
Testing React
import React from "react";
import { shallow } from "enzyme";
import { TodoContainer, myTasksKey } from "../TodoContainer";
describe("TodoContainer", () => {
it("should read from localStorage on mount", () => {
const expectedTasks = [{ text: "some task" }];
localStorage.setItem(myTasksKey, JSON.stringify(expectedTasks));
const wrapper = shallow(<TodoContainer />);
expect(wrapper.find("TodoList").prop("tasks")).toMatchObject(expectedTasks);
});
});
shallow vs mount
it("pressing enter with the input focused should submit it", () => {
const onChange = jest.fn();
const onSubmit = jest.fn();
const { getByTestId } = render(
<TodoInput value="" onChange={onChange} onSubmit={onSubmit} />
);
const input = getByTestId("task-input");
input.focus();
fireEvent(
input,
new KeyboardEvent("keyDown", { key: "Enter", keyCode: 13, which: 13 })
);
expect(onSubmit).toHaveBeenCalledTimes(1);
});
Accordion component

Class
class Accordion extends React.Component {
state = { openIndex: 0 }
setOpenIndex = openIndex => this.setState({ openIndex })
render() {
const { openIndex } = this.state
return (
<div>
{this.props.items.map((item, index) => (
<div key={item.title}>
<button onClick={() => this.setOpenIndex(index)}>
{item.title}
</button>
{
index === openIndex
? (<div className="item">{item.description}</div>)
: null
}
</div>
))}
</div>
)
}
}
Testing implementation
test('setOpenIndex sets the open index state properly', () => {
const wrapper = mount(<Accordion items={[]} />)
expect(wrapper.state('openIndex')).toBe(0)
wrapper.instance().setOpenIndex(1)
expect(wrapper.state('openIndex')).toBe(1)
})
test('Accordion renders accordion item with the item description', () => {
const pikachu = {
title: 'detective', description: 'sounds like deadpool'
}
const bulbasaur = {
title: 'bulbasaur', description: 'is the number 1'
}
const wrapper = mount(<Accordion items={[pikachu, bulbasaur]} />)
expect(wrapper.find('.item').props().children).toBe(pikachu.description)
})
Testing implementation
PASS src/components/accordion/__tests__/index.js
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.388s, estimated 2s
False negatives
class Accordion extends React.Component {
state = { openIndexes: [0] }
setOpenIndex = openIndex => this.setState({ openIndexes: [openIndex] })
render() {
const { openIndexes } = this.state
return (
<div>
{this.props.items.map((item, index) => (
<div key={item.title}>
<button onClick={() => this.setOpenIndex(index)}>
{item.title}
</button>
{
openIndexes.includes(index)
? (<div className="item">{item.description}</div>)
: null
}
</div>
))}
</div>
)
}
}
False negatives
FAIL src/components/accordion/__tests__/index.js
● setOpenIndex sets the open index state properly
expect(received).toBe(expected) // Object.is equality
Expected: 0
Received: undefined
8 | const wrapper = mount(<Accordion items={[]} />)
9 |
> 10 | expect(wrapper.state('openIndex')).toBe(0)
| ^
11 |
12 | wrapper.instance().setOpenIndex(1)
13 |
at Object.toBe (src/components/accordion/__tests__/index.js:10:38)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 2 total
False negatives
test('setOpenIndex sets the open index state properly', () => {
const wrapper = mount(<Accordion items={[]} />)
expect(wrapper.state('openIndexes')).toBe(0)
wrapper.instance().setOpenIndex(1)
expect(wrapper.state('openIndexes')).toBe(1)
})
test('Accordion renders accordion item with the item description', () => {
const pikachu = {
title: 'detective', description: 'sounds like deadpool'
}
const bulbasaur = {
title: 'bulbasaur', description: 'is the number 1'
}
const wrapper = mount(<Accordion items={[pikachu, bulbasaur]} />)
expect(wrapper.find('.item').props().children).toBe(pikachu.description)
})
False negatives
PASS src/components/accordion/__tests__/index.js
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.388s, estimated 2s
False positives
class Accordion extends React.Component {
state = { openIndexes: [0] }
setOpenIndex = openIndex => this.setState({ openIndexes: [openIndex] })
render() {
const { openIndexes } = this.state
return (
<div>
{this.props.items.map((item, index) => (
<div key={item.title}>
<button onClick={() => this.setOpenIndex(index)}>
{item.title}
</button>
{
openIndexes.includes(index)
? (<div className="item">{item.description}</div>)
: null
}
</div>
))}
</div>
)
}
}
False positives
class Accordion extends React.Component {
state = { openIndexes: [0] }
setOpenIndex = openIndex => this.setState({ openIndexes: [openIndex] })
render() {
const { openIndexes } = this.state
return (
<div>
{this.props.items.map((item, index) => (
<div key={item.title}>
<button onClick={this.setOpenIndex}>
{item.title}
</button>
{
openIndexes.includes(index)
? (<div className="item">{item.description}</div>)
: null
}
</div>
))}
</div>
)
}
}
False positives
PASS src/components/accordion/__tests__/index.js
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.388s, estimated 2s
Testing with RTL
test('can open accordion items to see the description', () => {
const pikachu = {
title: 'detective', description: 'sounds like deadpool'
}
const bulbasaur = {
title: 'bulbasaur', description: 'is the number 1'
}
const {getByText, queryByText} = render(
<Accordion items={[pikachu, bulbasaur]} />,
)
expect(getByText(pikachu.description)).toBeInTheDocument()
expect(queryByText(bulbasaur.description)).toBeNull()
fireEvent.click(getByText(bulbasaur.title))
expect(getByText(bulbasaur.description)).toBeInTheDocument()
expect(queryByText(pikachu.description)).toBeNull()
})
Implementation with hooks
const Accordion = ({ items }) => {
const [openIndex, setOpenIndex] = useState(0)
return (
<div>
{items.map((item, index) => (
<div key={item.title}>
<div>
<button onClick={() => setOpenIndex(index)}>
{item.title}
</button>
</div>
{
index === openIndex
? (<div>{item.description}</div>)
: null
}
</div>
))}
</div>
)
}
Nothing changes :)
PASS src/components/accordion/__tests__/index.js
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.299s
Avoid implementation details and your tests will work with hooks or whatever the next thing will be
Enhanced Counter class
class EnhancedCounter extends React.Component {
state = {
count: Number(window.localStorage.getItem('count') || 0),
}
increment = () => this.setState(({count}) => ({count: count + 1}))
componentDidMount() {
window.localStorage.setItem('count', this.state.count)
}
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
window.localStorage.setItem('count', this.state.count)
}
}
render() {
return <button onClick={this.increment}>{this.state.count}</button>
}
}
afterEach(() => {
window.localStorage.removeItem('count')
})
test('counter increments the count', () => {
const {container} = render(<EnhancedCounter />)
const button = container.firstChild
expect(button.textContent).toBe('0')
fireEvent.click(button)
expect(button.textContent).toBe('1')
})
test('reads and updates localStorage', () => {
window.localStorage.setItem('count', 3)
const {container} = render(<EnhancedCounter />)
const button = container.firstChild
expect(button.textContent).toBe('3')
fireEvent.click(button)
expect(button.textContent).toBe('4')
expect(window.localStorage.getItem('count')).toBe('4')
})
Testing with RTL
PASS src/components/enhanced-counter/__tests__/index.js
✓ counter increments the count (26ms)
✓ reads and updates localStorage (3ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.42s
Testing with RTL
import React, {useState, useEffect} from 'react'
const EnhancedCounter = () => {
const [count, setCount] = useState(() =>
Number(window.localStorage.getItem('count') || 0),
)
const incrementCount = () => setCount(c => c + 1)
useEffect(() => {
window.localStorage.setItem('count', count)
}, [count])
return <button onClick={incrementCount}>{count}</button>
}
export default EnhancedCounter;
Testing with RTL
PASS src/components/enhanced-counter/__tests__/index.js
✓ counter increments the count (26ms)
✓ reads and updates localStorage (3ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.42s
Custom hooks
import { useState } from 'react';
const useCounter = () => {
const [count, setCount] = useState(0)
const increment = () => setCount(currentCount => currentCount + 1)
return {count, increment}
}
export default useCounter
// usage:
// const Counter = () => {
// const {count, increment} = useCounter()
// return <button onClick={increment}>{count}</button>
// }
Custom hooks
const TestHook = ({ callback }) => {
callback()
return null
}
test('useCounter', () => {
let count, increment
render(<TestHook callback={() => ({count, increment} = useCounter())} />)
expect(count).toBe(0)
act(() => {
increment()
})
expect(count).toBe(1)
})
react-hooks-testing-library
import { renderHook, act } from 'react-hooks-testing-library'
test('useCounter', () => {
const { result } = renderHook(() => useCounter())
expect(result.current.count).toBe(0)
act(() => result.current.increment())
expect(result.current.count).toBe(1)
})
react-hooks-testing-library
PASS src/components/use-counter/__tests__/index.js
✓ useCounter (2ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.213s, estimated 1s
React Lazy/Suspense
React Lazy/Suspense
import React from 'react';
const LazyComponent = React.lazy(() => import('./lazy')); // export default () => <div> I am lazy </div>
const Wrapper = () => {
return (
<div>
<div> Lazy component here: </div>
<LazyComponent />
</div>
)
}
export default Wrapper
React Lazy/Suspense
FAIL src/components/lazy-test/__tests__/index.js
✕ renders lazy (6ms)
● renders lazy
A React component suspended while rendering, but no fallback UI was specified.
Add a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display.
in Unknown (at lazy-test/index.jsx:9)
in div (at lazy-test/index.jsx:7)
in Wrapper (at __tests__/index.js:7)
5 |
6 | test('renders lazy', () => {
> 7 | const { debug } = render(<Wrapper />);
| ^
8 | debug();
9 | })
React Lazy/Suspense
test('renders lazy', () => {
const { getByText } = render(
<React.Suspense fallback="loading...">
<Wrapper />
</React.Suspense>
);
expect(getByText('I am lazy')).toBeInTheDocument();
})
React Lazy/Suspense
FAIL src/components/lazy-test/__tests__/index.js
✕ renders lazy (8ms)
● renders lazy
Unable to find an element with the text: I am lazy. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
<body>
<div>
<div
style="display: none;"
>
<div>
Lazy component here:
</div>
</div>
loading...
</div>
</body>
> 13 | expect(getByText('I am lazy')).toBeInTheDocument();
| ^
React Lazy/Suspense
import { render, wait } from 'react-testing-library';
test('renders lazy', async () => {
const { getByText } = render(
<React.Suspense fallback="loading...">
<Wrapper />
</React.Suspense>
);
await wait();
expect(getByText('I am lazy')).toBeInTheDocument();
})
React Lazy/Suspense
PASS src/components/lazy-test/__tests__/index.js
✓ renders lazy (23ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.51s, estimated 1s
React Lazy/Suspense
import { render, waitForElement } from 'react-testing-library';
test('renders lazy', async () => {
const { getByText } = render(
<React.Suspense fallback="loading...">
<Wrapper />
</React.Suspense>
);
const lazyElement = await waitForElement(() => getByText('I am lazy'));
expect(lazyElement).toBeInTheDocument();
})
Testing React
PASS src/components/lazy-test/__tests__/index.js
✓ renders lazy (75ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.486s, estimated 1s
Thx for watching
Questions?
react-testing-library
By Rafael Vitor
react-testing-library
- 1,756