Skip to main content

Testing in React with Jest

Learning Objectives

  • Discuss the features of Jest and Enzyme
  • Finish setting up a development environment with create-react-app
  • Implement test driven development processes
  • Use Jest and Enzyme to test React applications

Framing

We all run tests with each line of code we write. That test is did the thing work as expected or did it break something. Although we can't truly escape writing our code in this fashion there is a flaw in that approach and one we have all experienced at some point. Fixing one problem breaks something else all together.

This is where formal testing comes in. It ensures that your app is continuing to function as expected, and can save you a massive headache down the road, a few of which are:

  • Ensuring that the code continues to yield the desired result, and does not break with further development.
  • Testing for various use cases, rather than just the one that you performed.
  • Reminding you why code is written a certain way (to handle the finicky third-party package rendering, or ensure that functions come to completion in the proper order, etc.)

Testing Libraries

We will be using the following libraries to create and run our tests.

Jest and React Testing Library are used very much in conjunction with one another, and it is nearly impossible to talk about one without the other. You can think of Jest as doing the actual testing, while React Testing Library recreates the thing to be tested

Enzyme is used to mimic JQuery's DOM manipulation library to make testing React even easier. It allows us to grab the state of the component, simulate user actions, and grab elements from the virtual DOM.

Starting With Jest

Jest is an easy to configure testing framework built by Facebook for testing JavaScript code. It's one of the most popular ways to test React components nowadays.

Running A Single Test

Test are created using either it() or test(). Both functions perform the same action and in the end it comes down to readability and more so about the actual test message.

test("does this thing", () => {});

// VS

it("should do this thing", () => {});

Creating A Suite Of Tests

Several tests can be combined into a suite of tests by wrapping them in describe().

Here is an example of several tests being run against an algo that flattens an array.

// TEST SUITE
describe("Flatten Array Tests", () => {
// TEST CASE
it("should flatten an array.", () => {
// test goes here
});

it("should return an empty array if the input is an empty array.", () => {
// test goes here
});
});

⏰ Activity - 3min

Let's take a look at the docs on running tests in CodeSandbox

--

Starter Code

The starter code we will be using is a full create-react-app build and is slightly different then the React app we are able to spin up using the React CodeSandbox template.

Here is the starter code: CodeSandbox React Testing

Flatten An Array Test

Let's write our first test. Inside the src folder there is a folder called Algos which contains two files: flatten.js and flatten.test.js

If we take a look at flatten.js we will see it contains the code needed to flatten an array of nested arrays.

const flatten = (arr, result = []) => {
arr.forEach((elm) => {
switch (true) {
case Array.isArray(elm):
flatten(elm, result);
break;
default:
result.push(elm);
}
});
return result;
};

export default flatten;

This code would indeed return a flattened array. We can even test this out in this repl.

flatten([1, [2, 3], [[4], 5]]); // => [(1, 2, 3, 4, 5)]

Our First Test

If we take a look at flatten.test.js we see that contains no code.

Since there are no tests written our testing results should be empty.

Empty Test Result

Of course the idea here is that the user would need to figure the code to the algo themselves and then run the tests to validate they got it right.

Our goal however is to create a series of tests that would validate the results of running that code.

Let's start by creating a single test that includes the input and out results

it("should flatten an array of arrays.", () => {
const nestedArray = [1, [2, 3], [[4], 5]];
const flatArray = [1, 2, 3, 4, 5];
});

Now we use the expect() function that then runs the algo passing in the array to be flattened and then compares the results.

it("should flatten an array of arrays.", () => {
const nestedArray = [1, [2, 3], [[4], 5]];
const flatArray = [1, 2, 3, 4, 5];
expect(flatten(nestedArray)).toEqual(flatArray);
});

Since we know the algo does what it's supposed to do we should see that our test has passed.

1 Test Pass

Let's force our test to fail so we can see the results of a failed test.

Test Failed


⏰ Create Test Activity - 3min

Now it's your turn to create the following tests:

  • it should return an empty array when the input is an empty array
  • it should return a flattened array if the input contains 4 levels of nested arrays

ADD SOLUTION CODE HERE

Solution
it("Returns empty array when the input is an empty array.", () => {
const array = [];
const result = flatten(array);
const expectedResult = [];
expect(result).toEqual(expectedResult);
});

it("Goes 4 levels deep.", () => {
const nestedArray = [[1, 2, [3, [[4], 5]]], [6]];
const flatArray = [1, 2, 3, 4, 5, 6];
const result = flatten(nestedArray);
expect(result).toEqual(flatArray);
});

Test Suites

Being that these tests are meant to test the validity of the same code we could have placed them in a describe() so they are viewed as a suite of tests.

describe("Flatten tests:", () => {
it("should flatten an array of arrays.", () => {
// ...code
});

it("should returns empty array when the input is an empty array.", () => {
// ...code
});

it("should flatten an array that is at least 4 levels deep.", () => {
// ...code
});
});

Testing Using The React Testing Library

Testing in React uses the same approach for creating individual tests and organizing them into a suite of tests.

We do however need to leverage the React Testing Library and, for even more advanced testing we would need the assist of Enzyme.

Setup

Let's first create a filed called App.test.js and import both React and the App component.

App.test.js
import React from "react";
import App from "./App";

We will need to import both render and screen from the testing library.

App.test.js
import React from "react";
import App from "./App";
import { render, screen } from "@testing-library/react";

⏰ Documentation Activity - 3min

Let's take a minute to look at the documentation for screen as it will hold the DOM elements and run the query.


Writing Our Test

Our basic test will confirm that the text "learn react" is being rendered via the App component.

For that we will need to render the component and then query the page for the text using screen.getByText()

App.test.js
import React from 'react'
import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
// RENDERS THE APP COMPONENT
render(<App />);
// EXAMINES THE TEXT ON THE PAGE LOOKING FOR 'learn react'
const linkElement = screen.getByRole("link", /learn react/i);

// LETS SEE WHAT THIS RETURNS
screen.debug('linkElement', linkElement);
});

Here is what linkElement returns:

LinkElement Screenshot

In order to confirm that the test meets our expectations we use the expect() function as before.

App.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import App from "./App";

test("renders learn react link", () => {
render(<App />);
const linkElement = screen.getByRole("link", /learn react/i);
// THE EXPECT METHOD TO CONFIRMS THAT THE TEXT IS IN THE DOCUMENT
expect(linkElement).toBeInTheDocument();
});

expect().toBeInTheDocument

What is Enzyme?

Enzyme mimics JQuery's DOM manipulation library to make testing React easier. Using Enzyme we can also directly test state within a class based component (it doesn't yet support hooks) which the React Testing Library cannot do.

Setting Up Our Environment For Enzyme

Now the React Testing Library comes completely configured within create-react-app, so we don't have to do anything else to get it working however Enzyme requires a bit of setup.

First we need to import the following packages:

  • enzyme
  • enzyme-adapter-react-16
  • react-test-renderer

Then we need to create a file setupTests.js. Create-react-app reads this file to see if there is any additional setup for the tests. In that file let's import the jest-dom library and then configure Enzyme to use an Adapter

At the moment, Enzyme has adapters that provide compatibility with React 16.x, React 15.x, React 0.14.x and React 0.13.x. Since we are using the most current version of React lets configure the adapter to use v16.

setupTests.js
import "@testing-library/jest-dom";
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";

configure({ adapter: new Adapter() });

export default Adapter;

We just need to keep in mind that we will need to import this file into any of our *.test.js files that require Enzyme.

Writing Tests Using Enzyme

Inside the components folder we will find both Counter and HelloWorld. We will be using these folder to organize our code for testing.

We will start with HelloWorld and need 2 files, one for our Component and the other to run our tests.

  • src/components/HelloWorld/HelloWorld.js
  • src/components/HelloWorld/HelloWorld.test.js

As before React will detect that there is a test but since there isn't anything in it nothing will happen.

No Assert in Test

Testing Props

Let's write a test that confirm the HelloWorld component renders out a name that's passed to it via props.

Enzyme tests begin with rendering a React component, and for this, you have three choices:

  • Shallow Rendering
  • Full DOM Rendering
  • Static Rendering

Let's start with Shallow Rendering as it should be used for tests that are limited in scope to the component being tested and will not need to test lifecycle method nor render any of it's children.

Initial Setup

Let's start by importing React, shallow and the HelloWorld component.

HelloWorld.test.js
import React from "react";
import Adapter from "../../setUpTests";
import { shallow } from "enzyme";
import HelloWorld from "./HelloWorld";

Now we write the tests we would like to perform and place them in a test suite.

HelloWorld.test.js
//...previous imports

describe("Hello world component", () => {
it("should render props as expected", () => {});
});

Here we will be testing to confirm that the HelloWorld component was passed a prop value of Your name.

HelloWorld.test.js
//...previous imports

describe("Hello world component", () => {
it("should render props as expected", () => {
const component = shallow(<HelloWorld name={"Your name"} />);
expect(component.contains("Your name")).toBe(true);
});
});

The testing engine should rerun automatically and this time the App and flatten tests passes but not HelloWorld.

2 Pass 1 Fail Test

Of course the reason being that we haven't written the actual HelloWorld Component as of yet.

Let's write the minimum amount of code needed for it to pass. In this example, we just need a component that renders prop.name.

HelloWorld.js
import React from "react";

const HelloWorld = (props) => <h1>{props.name}</h1>;

export default HelloWorld;

The test passes without the need to import and run the component in App.

3 Passing Tests

Writing Tests for a Counter App

For this exercise, you will be using test driven development to write the React code to pass some pre-written tests.

We want to build a counter app. When we press a button, we want a number stored in state to increase, and when we press a second button that number will decrease. Given these test requirements, write a React component that passes the following tests.

Initial Setup for Counter

Let's create a folder and some files for our counter app.

  • src/components/Counter/Counter.jsx
  • src/components/Counter/Counter.test.jsx

Copy the following code into the Counter.jsx component and we will write our tests to validate this code.

Live Editor
Result
Loading...

Copy the following code into Counter.test.jsx to get us started.

Counter.test.jsx
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it } from "@jest/globals"
import "@testing-library/jest-dom/extend-expect";

import Counter from "./Counter";

describe("Counter component", () => {});

Here are the functions that are being used in case we want to review the Jest and Enzyme documentation and get a better idea of what they are doing.

Jest

Enzyme

The Tests

Here are the tests we will create to test the Counter component and it's functionality.

  • should have a header that says "Counter"
  • should display the current number
  • should have a button with a class name of plus that increases the number
  • should have a button with a class name of minus that decreases the number

The Header Test

If we examine the Counter component we can see that it contains an h1 with the text Counter and that is what we will run our test against.

Counter.test.jsx
describe("Counter component", () => {
it('should have a header that says "Counter"', () => {
render(<Counter />);

expect(
screen.getByRole("heading", { name: /Counter/i, level: 1 })
).toBeInTheDocument();
});
});

The Counter Test

If we examine the Counter component we can see that it contains a span with a class name of 'counter` and that it should show the current value and that is what we will run our test against.

Counter.test.jsx
describe("Counter component", () => {
// ...previous tests

it("should display the current number", () => {
render(<Counter />)
expect(screen.getByText(/0/i)).toBeInTheDocument();
});
});

Adding beforeEach()

Since we will need to recreate the Counter component for each test we can create a shallow version of it prior to running each subsequent test.

To do this we must use the beforeEach() function and then we update each test and remove the local shallow copy.

Counter.test.jsx
import React from "react";

import Counter from "./Counter";

describe("Counter component", () => {
beforeEach(() => {
render(<Counter />)
})

it('should have a header that says "Counter"', () => {
expect(
screen.getByRole("heading", { name: /Counter/i, level: 1 })
).toBeInTheDocument();
});

it("should display the current number", () => {
expect(screen.getByText(/0/i)).toBeInTheDocument();
});
});

Now let's continue with our tests.

The Increment Using Button Test

Testing buttons requires that we fire a click event and cause it's functionality to execute. We can do this using .simulate() and confirm that the value has increased by 1.

Counter.test.jsx
describe("Counter component", () => {
// ...previous tests
it("should have a '+' button that increases the number", () => {
render(<Counter />);
userEvent.click(screen.getByRole("button", { name: "+" }));
expect(screen.getByText(/1/i)).toBeInTheDocument();
});
});

⏰ Activity - 2min

The Decrement Using Button Test

Write the following test:

  • it should have a '-' button that decreases the number

Solution
Counter.test.jsx
describe('Counter component', () => {
// ...previous tests
it("it should have a '-' button that decreases the number", () => {
render(<Counter />);
userEvent.click(screen.getByRole("button", { name: "-" }));
expect(screen.getByText(/-1/i)).toBeInTheDocument();
});
})

CodeSandbox Solution

https://codesandbox.io/s/rctr-react-testing-forked-v1ccw?file=/src/components/Counter/Counter.test.js

Bonus (Time Permitting): To Do List App

As a bonus let's now create a ToDo list app using test driven development. First let's create our files.

We will have two components: a ToDos.jsx component which will hold individual Todo.jsx components.

  • src/components/ToDos/ToDos.jsx
  • src/components/ToDo/ToDo.jsx

And all of our tests will be performed on the ToDos component so lets create the test file.

  • src/components/ToDos/ToDos.test.jsx

Now let's scaffold the configuration for our testing file.

ToDos.test.jsx

Let's add the following code to the test file.

ToDo.test.jsx
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it } from "@jest/globals";
import "@testing-library/jest-dom/extend-expect";

import ToDos from "./ToDos";

describe("ToDos Component", () => {
const listItems = [
{ task: "create lesson", done: false },
{ task: "clean apartment", done: false },
];

beforeEach(() => {
render(<ToDos tasks={listItems} />);
});

// add tests here
});

This looks pretty similar to our other testing blocks.

Counting Subcomponennts

This time we will write our tests first and then the actual code that would validate the test.

ToDos.test.jsx
it("should contain two todo subcomponents", () => {
expect(screen.getAllByRole("listitem").length).toBe(2);
});

Let's write the minimum amount of code to make this test pass:

ToDos.jsx

ToDos.jsx
import React, { useState } from "react";
import ToDo from "./ToDo";

const ToDos = (props) => {
return (
<ol>
{props.tasks.map((task, idx) => (
<ToDo task={task} key={idx} />
))}
</ol>
);
};


export default ToDos;

ToDo.jsx

Live Editor
Result
Loading...

Test Rendering The Todo Components

ToDos.test.jsx
it("should render the todo list tasks", () => {
const items = screen.getAllByRole("listitem");
const todoNames = items.map((item) => item.textContent);
expect(todoNames).toEqual(["create lesson", "clean apartment"]);
});

The code to pass this one is pretty minimal.

ToDo.jsx Details

Live Editor
Result
Loading...

Now let's create functionality for making a new list item.

ToDos.test.jsx
it("should have a label and input and button", () => {
expect(screen.getByLabelText(/New Todo/i)).toHaveValue("");
expect(screen.getByRole("button", { name: /New/i })).toBeInTheDocument();
});

Note that we can create the actual HTML elements ToDos.js

ToDos.jsx
const ToDos = (props) => {
return (
<>
<label htmlFor="newTodo">New Todo</label>
<input id="newTodo" />
<button>New</button>
<ol>
{props.tasks.map((task, idx) => (
<ToDo task={task} key={idx} />
))}
</ol>
</>
);
};

⏰ Activity - 20min

You Do: Finish To Do App

Write the following tests. After writing a test, implement the React code to pass that test.

  • Should create a new todo on the click of a button and update the UI with it
ToDos.test.jsx
it("adds a new todo", () => {
const input = screen.getByLabelText(/New Todo/i);
userEvent.type(input, "Testing");
userEvent.click(screen.getByRole("button", {name: /New/i}));

expect(screen.getAllByRole("listitem").length).toBe(3);
});
  • Should mark todos as done on the click of a button
Solution

ToDos.jsx
import React from "react";

const ToDo = ({ task, markComplete }) => {
return (
<div>
<button className="mark-done" onClick={(e) => markComplete(task)}>
Mark as Complete
</button>
<div className={`task-name ${task.done ? "checked" : "unchecked"}`}>
{task.task}
</div>
</div>
);
};

export default ToDo;
ToDos.jsx
import React, { useState } from "react";

import ToDo from "./ToDo";

function ToDos(props) {
const [newTodo, setNewTodo] = useState("");
const [toDos, setToDos] = useState(props.tasks);

const handleChange = (e) => {
setNewTodo(e.target.value);
};

const createToDo = (e) => {
setToDos([...toDos, { task: newTodo, done: false }]);
setNewTodo(e.target.value);
};

const markComplete = (todo) => {
let toDosArray = [...toDos];
let index = toDosArray.indexOf(todo);
toDosArray[index].done = !toDosArray[index].done;
setToDos(toDosArray);
};

return (
<div>
<input onChange={handleChange} />
<button onClick={createToDo} className="new-todo">
create
</button>
{toDos.map((task, idx) => (
<ToDo task={task} markComplete={markComplete} key={idx} />
))}
</div>
);
}

export default ToDos;
ToDos.test.jsx
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it } from "@jest/globals";
import "@testing-library/jest-dom/extend-expect";

import ToDos from "./ToDos";
import ToDo from "./ToDo";

describe("ToDos Component", () => {
const listItems = [
{ task: "create lesson", done: false },
{ task: "clean apartment", done: false },
];

beforeEach(() => {
render(<ToDos tasks={listItems} />);
});

it("should have a label and input and button", () => {
expect(screen.getByLabelText(/New Todo/i)).toHaveValue("");
expect(screen.getByRole("button", { name: /New/i })).toBeInTheDocument();
});

it("should contain two todo subcomponents", () => {
expect(screen.getAllByRole("listitem").length).toBe(2);
});


it("should render the todo list tasks", () => {
const items = screen.getAllByRole("listitem");
const todoNames = items.map((item) => item.textContent);
const expectedNames = listItems.map((item) => item.task);
expect(todoNames).toEqual(expectedNames);
});

it("adds a new todo", () => {
const input = screen.getByLabelText(/New Todo/i);
userEvent.type(input, "Testing");
userEvent.click(screen.getByRole("button", {name: /New/i}));

expect(screen.getAllByRole("listitem").length).toBe(3);
});

it("Should mark todos as done when clicked", () => {
// component.find(".mark-done").at(0).simulate("click");
// expect(component.find(ToDo).filter((task) => task.done).length).toBe(1);
});
});

Review

  • What is Jest? How about Enzyme?

Resources