Log In

Testing react components


by Yariv Katz

In this lesson we will learn about unit testing your react components. We are going to cover the libraries that will help us with the testing along with the testing framework used for testing react components.

The importance of unit testing

Proper unit testing is major step towards a better quality web application. Combining unit testing along with a CI tool like Jenkins, makes our app less buggy and our product will manage to update more frequently and move quicker from development to production. I visited a lot of companies and most of the places I visited had poor practices for unit testing where most of them did not write unit tests at all on the frontend web side. Most of the reasons I got for people not to do test is the lack of time, and in my opinion when you start feeling comfortable with the tools you have for testing, it will become quite easier and quick to write proper tests. For that reason I think this tutorial is super important cause our main goal is to feel comfortable with the unit testing tools. In this lesson we will create 2 react components and test them:
- TodoList - The first component will display a list of todo tasks from our todo rest server located at the url: https://nztodo.herokuapp.com/api/task/?format=json
- TodoForm - The second component will contain a form with validation that will send a post request to our server and create a new todo task.
We will test the following:
- TodoList - we will test that if the server returns 3 items our list contains 3 elements.
- TodoForm - We will test that the form validation is working (we will us Formik for building our form and Yup for validation). Also we will test that when creating a new item a success message is posted.

Creating TodoList and TodoForm

First let's create our test subjects and create our two react components: TodoList and TodoForm. Create a new react project using the create-react-app cli. In the terminal type:

> npx create-react-app react-testing-tutorial
> cd react-testing-tutorial

In the src folder, create a folder for your components called components. In the components folder, create a folder todo-list to hold the TodoList component. In that folder, create a new js file called TodoList.js add the following code to that file:

import React, {Component} from 'react';

export default class TodoList extends Component {
    state = {
        tasks: []
    }

    async componentDidMount() {
        const res = await fetch('https://nztodo.herokuapp.com/api/task/?format=json');
        const json = await res.json();
        this.setState({
            tasks: json
        });
    }

    render() {
        return (
            <ul>
                {
                    this.state.tasks.map((singleTask) => 
                        <li key={singleTask.id}>{singleTask.title}</li>)
                }
            </ul>
        )
    }
}

When this component is mounted, it will send a request to grab the tasks from the server and populate the state with the tasks the server returned. We will then iterate over the tasks and print them in a ul list.

Let's create the 2nd component the TodoForm. For this component we will have a form built with Formik and validation with Yup. We will have to install using npm those libraries. In the terminal type:

> npm install formik yup --save

In the components folder, create a folder for your component called: todo-form. In that folder create a js file for your component called: TodoForm.js with the following code:

import React, {Component} from 'react';
import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';

const TaskSchema = Yup.object().shape({
    title: Yup.string()
        .min(2)
        .max(100)
        .required(),
    description: Yup.string()
        .required(),
    group: Yup.string()
        .matches(/[a-z]+/)
        .required(),
    when: Yup.date().default(() => new Date())
});

export default class TodoForm extends Component {
    state = {
        taskCreated: null
    }

    handleSubmit = async (values) => {
        const res = await fetch('https://nztodo.herokuapp.com/api/task/', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(values)
        });
        const json = await res.json();
        this.setState({
            taskCreated: json
        })
    }

    bootstrapField = (label, {field, form: {touched, errors}}) => {
        return (
            <div className="form-group">
                <label>{label}</label>
                <input {...field} className="form-control" />
                {
                    touched[field.name] &&
                    errors[field.name] &&
                    <div className="alert alert-danger">{errors[field.name]}</div>
                }
            </div>
        )
    }

    render() {
        return (
            <Formik
                initialValues={ {
                    title: '',
                    description: '',
                    group: '',
                    when: new Date()
                } }
                validationSchema={TaskSchema}
                onSubmit={this.handleSubmit}
            >
                {
                    () => (
                        <Form>
                            <Field 
                                type="text"
                                render={(fieldProps) => this.bootstrapField('Title', fieldProps)}
                                name="title"  />
                            <Field 
                                type="text"
                                render={(fieldProps) => this.bootstrapField('Description', fieldProps)}
                                name="description"  />
                            <Field 
                                type="text"
                                render={(fieldProps) => this.bootstrapField('Group', fieldProps)}
                                name="group"  />
                            <Field 
                                type="datetime-local"
                                render={(fieldProps) => this.bootstrapField('When', fieldProps)}
                                name="when"  />
                            <div className="form-group">
                                <button type="submit" className="btn btn-primary">
                                    Submit
                                </button>
                            </div>
                            {
                                this.state.taskCreated ? <div className="alert alert-success">Task created successfully</div> : null
                            }
                        </Form>
                    )
                }
            </Formik>
        )
    }
}

We created a form here with Formik and validation on the fields using Yup. If the form is passing validation we are then taking the values and sending a post request to create new task, if the task is created we set it in the task and display a success message.

We are using bootstrap for the form appearance so lets install bootstrap. On the terminal type:

> npm install bootstrap --save

the index.css file contains the global styles of our app. In that file lets import the bootstrap css. At the top of that file add the following:

@import "~bootstrap/dist/css/bootstrap.css";

Our components are finished let's start working on testing them.

Jest

Facebook created Jest for us to test our react application. Jest is a testing solution, similar to mocha and Jasmine. create-react-app is already configured to use Jest so let's write our first jest test. In the components folder inside the todo-list folder create a file called TodoList.test.js. Jest is already configured to run files that end in a *.test.js. In that file create the following:

test('my first jest test', () => {
    expect(2 + 2).toBe(4);
});

so to write test files we create a *.test.js file. Each test we want to run is using the test with the name of the test and the test function as the second argument. We can use expect and matchers to assert our test. We can group our tests using describe blocks and run before and after code using beforeAll, beforeEach, afterAll, afterEach. We can also use jest to mock out functions. To run our tests, in the terminal type:

> npm run test

Debugging our test

Lets try and debug the test we wrote with Visual Studio Code. That was actually a bit hard to achieve... In the menu choose: Debug -> Open Configurations.
In the configurations array add the following json:

{
    "type": "node",
    "request": "launch",
    "name": "Jest All",
    "program": "${workspaceFolder}/node_modules/.bin/jest",
    "args": ["--runInBand", "--env=jsdom"],
    "console": "integratedTerminal",
    "internalConsoleOptions": "neverOpen"            
}

We will also need to make jest transform our es6 files and JSX syntax so we will have to configure babel to work together with Jest. In the root folder add a .babelrc file with the following:

{
    "presets": [
        "@babel/preset-env", 
        "@babel/preset-react"
    ],
    "plugins": [
        "@babel/plugin-proposal-class-properties"
    ]
}

make sure you install those presets:

> npm install @babel/preset-env @babel/preset-react @babel/plugin-proposal-class-properties --save-dev

We will have to make jest ignore the imports to image files and css files, we will also need to make jest run a setup file before running the tests. We will add all those in a configuration file. You can ask jest to manufacture a template for that file with the command:

> jest --init

After the above command jest will create a jest.config.js file, modify it with the following configuration options:

module.exports = {
    moduleNameMapper: {
        "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
        "\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js"
    },

    setupFiles: [
        "<rootDir>/jest.init.js"
    ],

    testEnvironment: "node",

};

notice the moduleNameMapper will make the imports to images and styles go to mock files which we will have to create. In the root directory create a folder called __mocks__ and add the file fileMock.js with the following:

module.exports = 'test-file-stub';

Create the next mock import for style in the __mocks__ folder create the file styleMock.js with the following:

module.exports = {};

Now create the setupFile that will run before the test files. In the root directory create the file: jest.init.js with the following:

import 'babel-polyfill';

we will have to also install that package:

> npm install babel-polyfill --save-dev

You should be able to run your code with the visual studio code debugger and add breakpoints in your test files.

Enzyme

Jest is not enough... We also need a tool that will help us create our components in the test and interact with them. Enzyme is doing just that. It allows us to shallow create only the component we are testing, without the children, and without the need of a full dom like api, or if we want we can mount our component but in that case we will require so sort of DOM like behaviour in our tests. Lets install enzyme with the terminal and npm, we will also need to install an adapter for enzyme for the new version 16 of react:

> npm install enzyme enzyme-adapter-react-16 --save-dev

We will need to configure enzyme to work with react v16 using the adapter we installed, so modify the jest setup file we created earlier jest.init.js

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import 'babel-polyfill';

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

Testing TodoList

Our TodoList component is simple enough without child components and without too much interaction with DOM API's, so we can use enzyme's shallow rendering in order to test it. It is better if we isolate the test from the outside world and mock the actual server request, this will also gurentee stability of our test and the test will not crash if the server crash or sends a different result. Modify the TodoList.test.js.

import React from 'react';
import {shallow} from 'enzyme';
import TodoList from './TodoList';

test('TodoList should return 3 elements', async () => {
    // mock server returns 3 elements
    global.fetch = jest.fn().mockImplementation(() => Promise.resolve({json: () => Promise.resolve([
        {id: 1, title: 'todo1'},
        {id: 2, title: 'todo2'},
        {id: 3, title: 'todo3'},
    ])}));

    const wrapper = shallow(<TodoList />);
    const res = await fetch();
    const json = await res.json();
    expect(wrapper.find('li').length).toBe(3);
});

The intresting thing here is that we are mocking the fetch function to return a resolved promise, with an object with json containing another resolve promise of 3 tasks. We are then verifying that the component rendered 3 li elements. before we check the 3 li elements we are waiting for the res and json so we will know for sure that the component async code is resolved. We can use jest for mocking our global function and application services.

Testing the TodoForm

We want to create two tests here. The first one fills the form with a bad group and tries to submit the form to verify that the validation of the group is working. The second test we fill the form submit and send a proper success response from the server and verify that a success message is dispalyed. Since our TodoForm component is using children component Formik, Field, and since we are using browser api like submitting the form, we will need to use enzyme full mount of the component. In the todo-form folder, create the file TodoForm.test.js containing the following:

import React from 'react';
import {mount} from 'enzyme';
import TodoForm from './TodoForm';

test('group validation', async (done) => {
    const wrapper = mount(<TodoForm />);
    
    // set the inputs
    const instance = wrapper.find('Formik').instance();
    const changeState = new Promise((resolve) => {
        instance.setState({
            values: {
                title: 'test1',
                description: 'test1',
                group: '345345'
            }
        }, () => resolve())
    });
    await changeState;
    const form = wrapper.find('form');
    form.simulate('submit');
    setTimeout(() => {
        const alerts = wrapper.find('Formik')
            .update()
            .find('.alert');
        expect(alerts.length).toBe(1);
        done();
    });
});

test('success create', async (done) => {
    const wrapper = mount(<TodoForm />);

    global.fetch = jest.fn().mockImplementation(() => Promise.resolve({json: () => Promise.resolve(
        {id: 1, title: 'test1', description: 'test1', group: 'bugeez', when: (new Date()).toISOString()}        
    )}));
    
    // set the inputs
    const instance = wrapper.find('Formik').instance();
    const changeState = new Promise((resolve) => {
        instance.setState({
            values: {
                title: 'test1',
                description: 'test1',
                group: 'bugeez'
            }
        }, () => resolve())
    });
    await changeState;
    const form = wrapper.find('form');
    form.simulate('submit');

    setTimeout(() => {
        const alerts = wrapper.find('Formik')
            .update()
            .find('.alert-success');
        expect(alerts.length).toBe(1)
        done();
    });

});

In the first test we are grabbing the formik instance and set the state of formik in order to fill our form. Since the form is connected to formik via the state we can easily populate the form using formik state. We are then submitting the form and testing the expectation in a setTimeout so the component will be updated. In the second test we are mocking the post request and doing similar things to the first test only asserting that there is a success message.

Summary

The more comfortable you will feel with tests, the more tests you will write and the more stable your app will be. In this lesson we learned about jest and enzyme, we created components and wrote tests for those components, we also mocked server response so we will have a fake server in our tests.