📜 ⬆️ ⬇️

Convenient way to test React components

I wrote a custom report builder (custom reporter) for Jest and posted it on GitHub . My builder is called Jest-snapshots-book, it creates an HTML book of snapshots of React-application components.



The article will discuss what Jest is, snapshot testing, for which an additional report builder was needed and how to write them. Basically, all this relates to testing React components, but theoretically it can be used when working with any serializable data.

React-component paginator


For example, in the article we will test the paginator component ( Paginator ). It is part of our stocking project for creating serverless AWS applications ( GitHub ). The task of such a component is to display buttons for navigating through the pages of a table or something else.
')
This is a simple functional component with no state of its own (stateless component). As input, it receives from props the total number of pages, the current page and the function that handles clicking on the page. At the output, the component produces a shaped paginator. To display the buttons use another child component Button . If there are many pages, the paginator shows them not all, combining them and displaying them as dots.



Paginator component code
import React from 'react'; import classes from './Paginator.css'; import Button from '../../UI/Button/Button'; const Paginator = (props) => { const { tp, cp, pageClickHandler } = props; let paginator = null; if (tp !== undefined && tp > 0) { let buttons = []; buttons.push( <Button key={`pback`} disabled={cp === 1} clicked={(cp === 1 ? null : event => pageClickHandler(event, 'back'))}> ← </Button> ); const isDots = (i, tp, cp) => i > 1 && i < tp && (i > cp + 1 || i < cp - 1) && (cp > 4 || i > 5) && (cp < tp - 3 || i < tp - 4); let flag; for (let i = 1; i <= tp; i++) { const dots = isDots(i, tp, cp) && (isDots(i - 1, tp, cp) || isDots(i + 1, tp, cp)); if (flag && dots) { flag = false; buttons.push( <Button key={`p${i}`} className={classes.Dots} disabled={true}> ... </Button> ); } else if (!dots) { flag = true; buttons.push( <Button key={`p${i}`} disabled={i === cp} clicked={(i === cp ? null : event => pageClickHandler(event, i))}> {i} </Button> ); } } buttons.push( <Button key={`pforward`} disabled={cp === tp} clicked={(cp === tp ? null : event => pageClickHandler(event, 'forward'))}> → </Button> ); paginator = <div className={classes.Paginator}> {buttons} </div> } return paginator; } export default Paginator; 
Button Component Code
 import React from 'react'; import classes from './Button.css'; const button = (props) => ( <button disabled={props.disabled} className={classes.Button + (props.className ? ' ' + props.className : '')} onClick={props.clicked}> {props.children} </button> ); export default button; 

Jest


Jest is a well-known opensource library for unit testing JavaScript code. It was created and developed thanks to Facebook. Written on Node.js.

In general terms, the meaning of testing comes down to what you need to come up with the input parameters for your code and immediately describe the output that your code should produce. When executing tests, Jest executes your code with input parameters and checks the result against the expected one. If it coincided, the test will pass, and if not, it is not passed.

A small example from the site jestjs.io .

Suppose we have a Node.js module, which is a function that adds two numbers (file sum.js ):

 function sum(a, b) { return a + b; } module.exports = sum; 

If our module is saved in a file, to test it, we need to create a file sum.test.js , in which to write such code for testing:

 const sum = require('./sum'); test('adds 1 + 2 to equal 3', () => { expect(sum(1, 2)).toBe(3); }); 

In this example, using the test function, we created one test with the name 'adds 1 + 2 to equal 3' . By the second parameter to the test function, we pass the function, which actually performs the test.

The test is that we execute our sum function with input parameters 1 and 2 , and pass the result to the Jest expect () function. Then, using the Jest toBe () function, the transmitted result is compared with the expected ( 3 ). The toBe () function belongs to the Jest (matchers) check function category.

To perform the test, just go to the project folder and invoke jest on the command line. The jest will find the file with the .test.js extension and run the test. Here is the result he will produce:

 PASS ./sum.test.js ✓ adds 1 + 2 to equal 3 (5ms) 

Enzyme and snapshot component testing


Snapshot testing is a relative new feature in Jest. The point is that using a special check function, we ask Jest to save a snapshot of our data structure to disk, and in subsequent test runs compare new snapshots with previously saved ones.

The snapshot in this case is nothing but a simple textual representation of the data. For example, a snapshot of some object will look like this (the key of the array here is the name of the test):

 exports[`some test name`] = ` Object { "Hello": "world" } `; 

This is what the Jest check function looks like, which performs a snapshot comparison (parameters are optional):

 expect(value).toMatchSnapshot(propertyMatchers, snapshotName) 

The value can be any serializable data structure. For the first time, the toMatchSnapshot () function will simply write the snapshot to disk, the next times it will already perform the comparison.

Most often, this testing technology is used specifically for testing React-components, and even more accurately, for testing the correctness of rendering React-components. To do this, as a value, you need to pass the component after rendering.

Enzyme is a library that greatly simplifies testing React applications by providing convenient component rendering functions. Enzyme is developed in Airbnb.

Enzyme allows you to render components in code. To do this, there are several convenient functions that perform different rendering options:


Let's not delve into rendering options, for snapshot testing, static rendering is enough, which allows you to get static HTML code of a component and its child components:

 const wrapper = render(<Foo title="unique" />); 

So, we render our component and pass the result to expect () , and then call the .toMatchSnapshot () function. The it function is just a short name for the test function.

 ... const wrapper = render(<Paginator tp={tp} cp={cp} />); it(`Total = ${tp}, Current = ${cp}`, () => { expect(wrapper).toMatchSnapshot(); }); ... 

Each time the test is run, toMatchSnapshot () compares two snapshots: the expected one (which was previously recorded on the disc) and the current one (which was the result of the current test run).

If the pictures are identical, the test is considered passed. If there is a difference in the pictures, the test is considered not passed, and the user is shown the difference between the two pictures in the form of a diff (as in version control systems).

Here is an example of a Jest output when a test fails. Here we see that we have an additional button in the current snapshot.



In this situation, the user must decide what to do. If snapshot changes are planned due to component code changes, then it should overwrite the old snapshot with a new one. And if the changes are unexpected, then you need to look for the problem in your code.

I will give a complete example for testing the paginator (file Paginator.test.js ).

For more convenient testing of the paginator, I created the snapshoot (tp, cp) function, which will take two parameters: the total number of pages and the current page. This function will perform the test with the specified parameters. Then it remains only to call the snapshoot () function with various parameters (you can even in a loop) and test, test ...

 import React from 'react'; import { configure, render } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import Paginator from './Paginator'; configure({ adapter: new Adapter() }); describe('Paginator', () => { const snapshoot = (tp, cp) => { const wrapper = render(<Paginator tp={tp} cp={cp} />); it(`Total = ${tp}, Current = ${cp}`, () => { expect(wrapper).toMatchSnapshot(); }); } snapshoot(0, 0); snapshoot(1, -1); snapshoot(1, 1); snapshoot(2, 2); snapshoot(3, 1); for (let cp = 1; cp <= 10; cp++) { snapshoot(10, cp); } }); 

Why did you need additional report builder


When I started working with this technology of testing, the feeling of the unfinishedness of the initial approach did not leave me. After all, pictures can only be viewed as text.

But what if any component renders a lot of HTML code? Here is the paginator component, consisting of 3 buttons. A snapshot of this component will look like this:

 exports[`Paginator Total = 1, Current = -1 1`] = ` <div class="Paginator" > <button class="Button" > ← </button> <button class="Button" > 1 </button> <button class="Button" > → </button> </div> `; 

First you need to make sure that the original version of the component is rendered correctly. Not very convenient to do this, just looking at the HTML-code in text form. But these are just three buttons. And if you need to test, for example, a table or something more voluminous? And for full testing, you need to view a lot of pictures. It will be quite uncomfortable and hard.

Then, in the case of not passing the test, you need to understand the difference in the appearance of the components. Diff their HTML-code, of course, will allow you to understand what has changed, but again the opportunity to see firsthand the difference will not be superfluous.

In general, I thought that it would be necessary to make so that the pictures could be viewed in the browser in the same way as they look in the application. Including styles applied to them. So I got the idea to improve the snapshot testing process by writing an additional report builder for Jest.

Looking ahead, that's what I got. Every time my tests run, my builder updates the picture book. Directly in the browser, you can view the components as they appear in the application, as well as view the source code of the images and diff immediately (if the test fails).



Additional Jest Report Builders


The creators of Jest have provided the possibility of writing additional report builders. This is done as follows. You need to write a module on Node.JS, which must have one or more of these methods: onRunStart , onTestStart , onTestResult , onRunComplete , which correspond to various test progress events.

Then you need to connect your module in the jest config. For this there is a special directive reporters . If you want to additionally include your builder, then you need to add it to the end of the reporters array.

After that, the Jest will call the methods from your module at the onset of certain stages of the test, passing the current results to the methods. The code in these methods should actually create the additional reports that you need. This is in general terms the creation of additional report builders.

How does the Jest-snapshots-book


I do not specifically insert the module code into the article, as I will improve it further. It can be found on my GitHub, this is the src / index.js file on the project page.

My report builder is called upon completion of tests. I put the code in the onRunComplete method (contexts, results) . It works as follows.

In the results.testResults property the Jest passes an array of test results to this function. Each test result includes a path to the file with tests and an array of messages with the results. My report builder searches for the corresponding test file for each test file. If a snapshot file is found, the report builder creates an HTML page in the snapshot book and writes it to the snapshots-book folder in the root folder of the project.

To generate an HTML page, report builder uses the recursive function grabCSS (moduleName, css = [], level = 0) to collect all styles, starting with the component itself and further down the tree of all the components it imports. Thus, the function collects all the styles that are needed to correctly display the component. Collected styles are added to the snapshot book HTML page.

I use CSS modules in my projects, so I'm not sure that this will work if CSS modules are not used.

If the test is passed, the builder inserts an iFrame into the HTML page with a snapshot in two display options: the source code (snapshot as it is) and the component after rendering. The display option in iFrame is changed by a mouse click.

If the test was not passed, then everything is more complicated. The jest provides in this case only the message that it displays in the console (see the screenshot above).

It contains diffs and additional information about the failed test. In fact, in this case, we are essentially dealing with two shots: the expected one and the actual one . If we have the expected - it is stored on disk in the snapshot folder, then the current Jest snapshot does not provide.

So I had to write code that applies the Jest diff from the message to the expected snapshot and creates the actual snapshot based on the expected one. After that, the builder displays next to the iFrame of the expected iFrame snapshot of the current snapshot, which can change its content between three options: source code, component after rendering, diff.

This is how the report builder output looks like if you set the verbose = true option for it.



useful links



PS


Snapshot testing is not enough to fully test the React application. It covers only the rendering of your components. It is also necessary to test their functioning (reactions to user actions, for example). However, snapshot testing is a very convenient way to ensure that your components are rendered as intended. And jest-snapshots-book makes the process a little easier.

Source: https://habr.com/ru/post/421647/


All Articles