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 />)

  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")

  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.