The philosophy of react-testing-library
"The more your tests resemble the way your software is used, the more confidence they can give you." - Kent C. Dodds
This is the guiding principle in react-testing-library. The library helps you write tests that interact with your components in a way that the user would interact with the components. The library also focuses on testing without testing the implementation details. I will explore these ideas in this article and hopefully persuade you to give this amazing library a shot. Many thanks to Kent C. Dodds and all other contributors for this great piece of software.
Basics
As I show some examples below, please note that I am using Jest as my test runner. react-testing-library is a utility to test react components. It is not a replacement for Jest or other tools like Jest.
I will first render my component. I will do this using the render function, of course 😏, imported from react-testing-library. Useful functions are then returned from render that can be used to test our component. The components are fully rendered (not shallow rendered) therefore everything can be interacted with.
import React from 'react'
import { render, fireEvent } from 'react-testing-library'
import Counter from './Counter'
it("starts at 0 and it is incremented to 1", () => {
// Render takes a component (props can be passed in as well
// I am going to use getByTestId to access elements
const { getByTestId } = render(<Counter />)
// getByTestId will match elements by the attribute data-testid="count"
// here I saved two elements in their respective variables
const count = getByTestId('count')
const plusButton = getByTestId('plus')
// I can check properties on the element
// I want to make sure the tag is showing the count to be at 0 to begin
expect(count.textContent).toBe('0')
// fireEvent is a function that we imported from the library
// it allows us to click on the button the way the user would.
// I simply access the click function on it and pass a element for it to click
fireEvent.click(plusButton)
// Asserting that it now is now 1
expect(count.textContent).toBe('1')
})
The component that I tested is below. Other than placing the data-testid attributes, I did not test the implementation. I will show other methods of accessing elements, later in the article, that do not require placing data-testid attribute on.
import React from 'react'
class Counter extends React.Component {
state = {
count: 0
}
increment = () => {
this.setState(x => ({
count: x.count + 1
}))
}
render(){
return (
<div>
<h1 data-testid="count">{this.state.count}</h1>
<button data-testid="plus" onClick={this.increment}>+</button>
</div>
)
}
}
Below I have the same component refactored to use hooks and the test above does not break. This is extremely satisfying and something that might be harder to achieve when using other react testing utilities.
const Counter = () => {
const [count, setCount] = React.useState(0)
const increment = () => {
setCount(count + 1)
}
return (
<div>
<h1 data-testid="count">{count}</h1>
<button data-testid="plus" onClick={increment}>+</button>
</div>
)
}
More in depth example
The below example has a few new things I have not shown yet. Here I use getByPlaceholderText and getByLabelText to show how you can access elements without needing to place data-testid. I also use fireEvent.change to place a new value on the input in the way a user would.
import React from 'react'
import { render, fireEvent } from 'react-testing-library'
import Problem from './Problem'
it("has two inputs that I can change to see if it works", () => {
// Arrange
const { getByTestId, getByPlaceholderText, getByLabelText } = render(<Problem total={30}/>)
const checkResult = getByTestId('check')
// The getByPlaceholderText work as you would expect, the input can be found
// via the value of the PlaceholderText attribute
const input_one = getByPlaceholderText('Place the first number here!')
// The getByLabelText grabs the input by the label
const input_two = getByLabelText('Number 2')
const result = getByTestId('result')
// Act
fireEvent.change(input_one, { target: { value: 10 } })
fireEvent.change(input_two, { target: { value: 20 } })
fireEvent.click(checkResult)
// Asserting that it was correct
expect(result.textContent).toBe('Good')
})
Here is the component below. I used hooks because I love hooks! Check out the hooks documentation for more information on using them.
import React from 'react'
export default ({ total }) => {
const [input1, setInput1] = React.useState(0)
const [input2, setInput2] = React.useState(0)
const [status, setStatus] = React.useState(false)
const update1 = e => setInput1(Number(e.target.value))
const update2 = e => setInput2(Number(e.target.value))
const checkAnswer = () => setStatus(input1 + input2 === total)
return(
<div>
<label>Number 1
<input type="Number"
value={input1}
placeholder='Place the first number here!'
onChange={update1}/>
</label>
<h2>+</h2>
<label>Number 2
<input type="Number"
value={input2}
onChange={update2}/>
</label>
<h2>=</h2>
<h2>total</h2>
<button data-testid="check" onClick={checkAnswer}>Check Answer</button>
<h2 data-testid="result">{status ? 'Good' : 'Wrong'}</h2>
</div>
)
}
If you have multiple tests, it is important to import a function from react-testing-library called cleanup. This will remove components in between tests to prepare for the next render.
import { render, cleanup} from 'react-testing-library'
afterEach(cleanup)
If you are using hooks in your react components check out the documentation for when some special setup might be needed (due to the asynchronous nature of useEffect).
Conclusion
Check out the react-testing-library docs for more information. I have posted the project below where you can find my examples. I encourage everyone to use react-testing-library in your next project. Thank you for taking the time to read.
Top comments (0)