A Brief Guide to Test React Components with Hooks
Testing is a fundamental skill for every web developer to build high quality and stable web applications. But, it’s also intimidating especially for those who just getting started with web development. But it turns out: with modern frameworks and libraries, testing is not that hard.
A lot of developers avoid to test their applications, and you shouldn’t!
Getting started
To get started with this tutorial, you need to have a good understanding about the basics of React and React Hooks (e.g. useState
and useEffect
).
I also have prepared the code of this project. You can check it out right here.
Jest basics
Jest is a testing framework for JavaScript which focuses on simplicity. It works with other JavaScript frameworks like Angular and Vue, and most importantly, it works with React using create-react-app
out of the box!
To find out what is the purpose of jest, take a look at this function below.
function sum(a, b) {
return a + b
}
In this example, we have a sum
function, which just return a sum of two parameters. To test our function, define a test suite with test
method provided by jest. Then, we expect our function to return a sum of 2 + 2.
test("function should add numbers correctly", () => {
expect(sum(2, 2)).toBe(4)
})
When you’re writing tests, you often need to check some values meet certain conditions. In Jest, you use “matchers” which you can access them trough expect
function to validate different things.
Here are some basic matchers which you’ll frequently use in testing.
toBe
Use toBe
to compare primitive values. It calls Object.is
to compare values, which is in some cases better for testing than ===
strict equality operator.
For example, this test will validate some properties inside user
object:
const user = {
username: "johndoe",
age: 24,
}
test("is 24 years old", () => {
expect(can.age).toBe(24)
})
test("has a username", () => {
expect(can.name).toBe("johndoe")
})
toBeTruthy
Use toBeTruthy
when you just to check whether the value is true, no matter what the value is.
const user = {
username: "johndoe",
age: 24,
job: null,
isOld: false,
}
test("property is truthy", () => {
expect(can.username).toBeTruthy()
expect(can.age).toBeTruthy()
// This will throw error
expect(can.job).toBeTruthy()
expect(can.isOldEnough).toBeTruthy()
})
toBeFalsy
Just the opposite of toBeTruthy
.
const user = {
username: "johndoe",
age: 24,
job: null,
isOld: false,
}
test("the property is falsy", () => {
expect(can.job).toBeFalsy()
expect(can.isOldEnough).toBeFalsy()
// This will throw error
expect(can.username).toBeFalsy()
expect(can.age).toBeFalsy()
})
not
Use not
to expect the opposite of the given matchers.
const user = {
username: "johndoe",
age: 24,
job: null,
isOld: false,
}
test("property is truthy", () => {
expect(can.username).toBeTruthy()
expect(can.age).toBeTruthy()
expect(can.job).not.toBeTruthy()
expect(can.isOldEnough).not.toBeTruthy()
})
toHaveLength
Use toHaveLength
to check an object that has a length
property (e.g. Array, Object, and String) and it’s set to a certain number.
const numbers = [1, 2, 3]
const greeting = "Hello world!"
const user = { firstName: "John", lastName: "Doe" }
expect(numbers).toHaveLength(3)
expect(greeting).toHaveLength(12)
expect(user).toHaveLength(2)
React Testing Library
Another tool we use in this project is React Testing Library. It’s a simple and complete testing library for React that encourage us to write tests with good testing practices. If you’ve already familiar with React testing before, you should’ve heard about Enzyme. And it’s very similar to what we’re using right now.
There are several methods provided by React Testing Library which I found very useful and we’re going to cover in this tutorial.
Here is some quick guide using React Testing Library alongside with Jest.
render
Render component into a container which is appended to document.body
.
import { render } from "react-testing-library"
const { container } = render(<div />)
getByTestId
Find an element/component to test inside a container.
MyButton
component:
<button data-testid="my-button">Click Me!</button>
Our test will be:
const { getByTestId } = render(<MyButton />)
const myButton = getByTestId("my-button")
expect(myButton.text).toBe("Click Me!")
fireEvent
A method to fire DOM event.
// Fire a click event
fireEvent.click(getByTestId("my-button"))
// Fire a keyboard event
fireEvent.keyDown(getByTestId("my-input"), { key: "Enter", code: 13 })
cleanup
Unmounts React trees that were mounted with render.
test("renders into document", () => {
render(<MyComponent />)
// ...
cleanup()
})
Run cleanup
automatically after each test.
afterEach(cleanup)
test("renders into document", () => {
render(<MyComponent />)
// ...
})
Testing components
Here is an example of a component that uses hooks.
// App.js
import React, { useState } from "react"
function App() {
const [counter, setCounter] = useState(0)
return (
<div>
<h1>Counter: {counter}</h1>
<button onClick={() => setCounter(counter + 1)}>Increase</button>
</div>
)
}
Nothing special here, just a counter component with an h1
tag and a button
to increase the counter. So, how to test it?
We’re going to expect our “Increase” button will change the value inside the h1
tag whenever we click it. So, we need to grab the button
and the h1
element by using the getByTestId
method provided by React Testing Library. To make let it happen, we need to add data-testid
property inside both components.
<div>
<h1 data-testid="counter">Counter: {counter}</h1>
<button data-testid="increase-btn" onClick={() => setCounter(counter + 1)}>
Increase
</button>
</div>
Time to write some tests. First, we need to import our component and render it. Also, don’t forget cleanup after each test.
import { render, cleanup, fireEvent } from "react-testing-library"
import App from "./App.js"
afterEach(cleanup)
test("should increase counter when the button clicked", () => {
const component = render(<App />)
})
Second, grab our elements inside our component using getByTestId
method.
import { render, cleanup, fireEvent } from "react-testing-library"
import App from "./App.js"
afterEach(cleanup)
test("should increase counter when the button clicked", () => {
const { getByTestId } = render(<App />)
// New
const counter = getByTestId("counter")
const increaseBtn = getByTestId("increase-btn")
})
Finally, we expect our counter
element text is “Counter: 0” by default (coz our initial state is 0). Then, fire a click event to the increaseBtn
element and expect the counter
text increased to “Counter: 1”.
import { render, cleanup, fireEvent } from "react-testing-library"
import App from "./App.js"
afterEach(cleanup)
test("should increase counter when the button clicked", () => {
const { getByTestId } = render(<App />)
const counter = getByTestId("counter")
const increaseBtn = getByTestId("increase-btn")
// New
expect(counter.textContent).toBe("Counter: 0")
fireEvent.click(increaseBtn)
expect(counter.textContent).toBe("Counter: 1")
})
With those 3 simple steps, you’re now able to test your components that uses React Hooks.
Tags: Javascript, React, Hooks, Testing