Jon Knight
A few months ago I was hired to be the tester of a website. The problem? I had little experience with React, and to follow the test documentation from React, from and React Testing Library was quite complicated. So after I understood all of this, I decided to make this tutorial.
Another important thing to say, I tried to use Enzyme to do the tests, but that was not enough to easily do so. It wasn’t easier to read than React Testing Library, which is why I chose it in the end.
Introduction to the Basic SyntaxTo make the error report easily readable, we have three levels of descriptions in nameOfComponent.test.js — to refer to the component, the describe (to tag a group of tests by some context) and the it (which describes what that specific test and group of assertions (made with expect) refers to).
Then we have the render function, which is a method that puts that component in the Jest DOM, and puts it in renderResult. This is what we are going to use to make assertions. The render argument is a React element with its respective props. In this first code sample, I’ve entered the minimal props necessary to a valid DropDown using WAI-ARIA Design Patterns.
And lastly, we have the expects with two considerations:
What is “renderResult”?The best way to know what you are passing as an argument is to go to console.log(“ThingYouWant ToBeSure”), and check the output section of , because the terminal section might omit some important things.
The console.log(renderResult) is an object with several methods. One of them is the container that returns the DIV and the HTMLBodyElement, which has our DropDown component inside.
renderResult provides methods that make it easier to search for the elements in the DOM. That’s why we need the “get methods”, a.k.a queries, described in the .
Query APISince the children of the label are a label element in this component, the string rotulo is in the document. We can see it in the container.innerHTML console.log.
So the expect(renderResult.getByText(“lab1)).toBeInTheDocument() will return true.
The getByText query is appropriated to make an assertion in a unique string that visually appears in the HTML document.
The renderResult.container returns a pointer to a DOM Element, and we have access to the DOM API to search for the other related nodes, like these queries:
.getElementsByTagName(“span”)[0][“textContent”]
We’re getting the “children” text within the first span tag.
.getElementsByTagName(“div”)[0][“id”]The id from the first div.
.getElementsByTagName(“button”)[0].getAttribute(“aria-label”)The aria-label content from the first button.
.getElementsByClassName(“Extra”)[0]First Element with a class named Extra.
.getElementsByTagName(“div”)[1][“classList”].contains(“checkable”)If it’s in the second div it is ClassList which contains a “checkable” class.
Using only these examples, I could make all the assertions need for 12 WAI-ARIA pattern components — Checkbox, Date Input, DropDown, Radio, Button, ButtonGroup, Table, Tabs, Multiple Selection List, etc. However, this doesn’t apply to all kinds of props.
History, Router and FormsIn both cases we need to use a click event, to push this new state onto our memoryHistory and for the query to be put into location.search. To make the click happen we use getByText(“Home”) to make a reference to the span element.
It’s important to remember that the click event propagates to the link element (its parents). That’s why the click triggers the link and query methods, but the focus in the same context won’t. That’s why we deduce that a validation of a link component is:
fireEvent.click(RenderResult.getByText(“Home”)); expect(history.location.pathname).toBe(“/example”);And to test search parameters changes:
fireEvent.click(RenderResult.getByText(“Home”)); expect(history.location.search).toBe(“?author=Raissa”); expect(history.location.pathname).toBe(“/”);If you don’t know and don’t use React Router, I suggest you start using it.
Firing Events and the Use of StorybookFiring events demand two consideration. Which HTML element you must use to cause the event, and if the event has some parameters like an input.
The function props like onClick, onBlur, onKeyDown, etc should be well documented, so we know a which HTML element their listeners are. The tester is able to write a test code independently of the component’s implementation, because there are events, like focus, that don’t propagate to the parent.
To easily identify the HTML element, it’s basically the same as before — the use of console.log — to make the expects. To look for which events use and call the function prop, an idea is to use to document the component in isolation, its appearance and its behavior. This will make tests very real.
Firing Event ExamplesIt is always good to remember that it depends on your component! So here we will focus on the event, not the component.
Simple click on the span text, in a button.
const onClick = jest.fn(); fireEvent.click(RenderResult.getByText(“label”)); expect(onClick).toHaveBeenCalled();Hover to show content, like in a tooltip.
fireEvent.focus(RenderResult.getByLabelText(“Label”)); expect(RenderResult.getByText(“TooltipContent”)).toBeInTheDocument()Click on unselected option and a selected option in the multiple selection list.
fireEvent.click(RenderResult.getByText(“option 1”)); expect(SelectionFunc).toHaveBeenCalled(); fireEvent.click(RenderResult.getByText(“option 2”)); expect(UnselectionFunc).toHaveBeenCalled();Click to select the option and then click to submit it.
fireEvent.click(RenderResult.getByText(“option 1”)); fireEvent.click( RenderResult.container.getElementsByTagName(“button”)[0] ); expect(onChange).toHaveBeenCalled();Inserting a date on a DateInput to call its onChange mock function.
fireEvent.change(RenderResult.getByLabelText(“label”), { target: { value: “2019–06–25” } }); expect(onChange).toBeCalled();List of Event types:
to Be’sI’ve used only the ones below, but there are many others.
it(“Match Snapshot Button”, () => { const RenderResult = render(<Button>Hello</Button>); expect(RenderResult.getByText(“Hello”)).toBeInTheDocument(); expect(RenderResult).toMatchSnapshot(); });Dependencies and Test SetupI’ve done all the tests with just this import, and we’ve already discussed them before:
import React from “react”; import Component, { ComponentGroup } from “.”; import { render, fireEvent } from “react-testing-library”; import { Router } from “react-router-dom”; import { createMemoryHistory } from “history”; import { Form, Formik } from “formik”;In the src/setupTests.js configuration file, what matters is:
import “react-testing-library/cleanup-after-each”;To guarantee that each test won’t interfere with the next one (idempotents), because the React Trees mounted with render will be unmounted:
import “jest-dom/extend-expect”;Necessary for some specific asserts:
import “mutationobserver-shim”;Some tests need a . The modal is an example in our component.
Element.prototype.scrollIntoView = () => {};In the Multiple Selection List it was necessary to add a scrollIntoView mock function in the DOM, which doesn’t exist by default.
const originalConsoleError = console.error; console.error = message => { if (/(Failed prop type)/.test(message)) { throw new Error(message); } originalConsoleError(message); };This makes the prop type warning appear as test failure.
devDependencies in /package.json:
“jest-dom”: “3.2.2”, “jsdoc”: “3.5.5”, “mutationobserver-shim”: “0.3.3”, “react-testing-library”: “7.0.0”This is me.