JavaScript Frameworks play an important role in creating modern web applications. They provide developers with a variety of proven and well-tested solutions for creating efficient and scalable applications. Nowadays, it's hard to find a company that builds its frontend products without using whatever framework, so knowing at least one of them is a necessary skill for every frontend developer. To truly know a framework, nosotros need to understand not only how to create applications using it, just likewise how to test them.

JavaScript frameworks

At that place are lots of JS frameworks for creating web applications, and every year in that location are more than and more candidates to accept the leading position. Currently, iii frameworks play the main office: Angular, React and Vue. And in this article, I would like to focus on the near popular one - React.

To test or not to examination

When learning how to create applications using a given framework, developers often forget, or intentionally ignore, the necessity of testing already adult applications. As a result, you can often find complicated React applications with hundreds of components and cypher or very few tests, which never ends well.

In my opinion, testing is and then important because:

  • for well-tested applications, the chances are bang-up, that the developer will prepare whatever bugs before pushing to the repository,
  • having many well-written tests makes it easier for the developer to change the code, considering he immediately knows if the awarding is withal working properly,
  • nosotros tin can say that tests are too documentation - and what is even amend, such a documentation is always up to date, because when code is inverse and exam fails we take to update this test.

While testing React components may seem complicated at first, the sooner we start learning the easier it will be to practice information technology well over time and with little effort. In this post I would like to present one of the possibilities of testing React components.

React testing tools

The prepare of frameworks and tools for testing React applications is very big, so at the beginning of the testing adventure the question is: what to choose? Instead of list all the most popular tools, I would like to present those that I use every day, Jest + Enzyme.

Jest - One of the most popular (7M downloads each week) and very efficient JS testing framework, recommended by React creators.

Enzyme - React component testing tools. By calculation an abstraction layer to the rendered component, Enzyme allows y'all to manipulate the component and search for other components and HTML elements within it. Another advantage is too well-written documentation that makes it easier to commencement working with this tool.

This combination of the test framework (Jest), and the component manipulation tool (Enzyme) enables the creation of efficient unit of measurement and integration tests for React components.

React components and how to exam them

Enough theory, let's code!

I will beginning with a very basic component example together with tests. Afterward this component will be enriched with more advanced mechanisms (Router, Redux, Typescript) and I volition present how to adjust tests, so that they still work and pass.

Basic component

This is a simple React component example.

                              consign                const                UserInfoBasic                =                ({                user                })                =>                {                const                [                showDetails                ,                setShowDetails                ]                =                useState                (                false                );                const                renderUserDetails                =                ()                =>                (                <                div                className                =                {                styles                .                details                }                >                <                Typography                variant                =                "h5"                >Details</                Typography                >                <                p                >Login:                {                user                .                login                }                </                p                >                <                p                >Electronic mail:                {                user                .                e-mail                }                </                p                >                <                p                >Historic period:                {                user                .                historic period                }                </                p                >                </                div                >                )                const                getUserFullName                =                ()                =>                `                ${                user                .                name                }                                                ${                user                .                lastName                }                `                ;                const                toggleDetails                =                ()                =>                setShowDetails                (                !                showDetails                );                return                (                <                Paper                foursquare                =                {                truthful                }                className                =                {                styles                .                paper                }                >                <                Typography                variant                =                "h4"                >Info about user:                {                getUserFullName                ()                }                </                Typography                >                <                p                >First name:                {                user                .                name                }                </                p                >                <                p                >Last name:                {                user                .                lastName                }                </                p                >                <                Push button                onClick                =                {                toggleDetails                }                >                {                showDetails                ?                '                Hide                '                :                '                Show                '                }                user details</                Push                >                {                showDetails                &&                renderUserDetails                ()                }                </                Newspaper                >                );                };                          

The role of UserInfoBasic is to present user data. Component receives a user object in props and displays user info in two sections. The first section contains the first and last proper name and is visible all the time. The 2d section provides details and is hidden later on the first render, only can exist viewed by clicking a push button.

Y'all tin run across what is rendered by this component initially and later button click on these screenshots.

Basic component - initial state Basic component - after click

This component does non use any advanced React mechanisms or external libraries. Tests of such a component written with Jest + Enzyme are very uncomplicated and intuitive.

                              const                user                =                {                proper name                :                '                Darek                '                ,                lastName                :                '                Wojtowicz                '                ,                age                :                28                ,                login                :                '                dariuszwojtowicz                '                ,                email                :                '                dar.wojtowicz@mail service.com                '                };                describe                (                '                UserInfoBasic                '                ,                ()                =>                {                describe                (                '                Initial state                '                ,                ()                =>                {                const                wrapper                =                mountain                (                <                UserInfoBasic                user                =                {                user                }                />                );                test                (                '                should return header with full name                '                ,                ()                =>                {                expect                (                wrapper                .                find                (                Typography                ).                text                ()).                toContain                (                '                Info most user: Darek Wojtowicz                '                );                });                test                (                '                should not return details                '                ,                ()                =>                {                expect                (                wrapper                .                findWhere                ((                n                )                =>                due north                .                text                ()                ===                '                Login: dariuszwojtowicz                '                ).                length                ).                toEqual                (                0                );                });                test                (                '                should render "show user details" push button                '                ,                ()                =>                {                expect                (                wrapper                .                observe                (                Push button                ).                text                ()).                toEqual                (                '                Show user details                '                );                });                });                draw                (                '                After "Show user details" push button click                '                ,                ()                =>                {                const                wrapper                =                mount                (                <                UserInfoBasic                user                =                {                user                }                />                );                wrapper                .                find                (                Button                ).                simulate                (                '                click                '                );                test                (                '                should render details                '                ,                ()                =>                {                expect                (                wrapper                .                findWhere                ((                north                )                =>                n                .                text                ()                ===                '                Login: dariuszwojtowicz                '                ).                length                ).                toEqual                (                1                );                });                test                (                '                should return "Hibernate user details" push button                '                ,                ()                =>                {                expect                (                wrapper                .                find                (                Button                ).                text                ()).                toEqual                (                '                Hide user details                '                );                });                });                });                          

First we define the user object, which nosotros pass in the props of the tested component in all tests. Then nosotros use the describe methods of the Jest framework to split the tests into logical sets. In this case, ii sets have been defined.

The first one, Initial country, is used to exam the component in the initial country, without user interaction. The 2nd one is for testing the component later on clicking the Show user details push button. Other Jest framework methods used here are the examination method, which is used to write a single test, and the look method, which checks if the condition nosotros set is met.

In order to test a component, we need to have an instance of it, we need to return information technology somehow. This is where Enzyme comes in handy, which we tin hands use to simulate creating and rendering a component. We can apply 1 of the 3 methods (shallow, mount or render) to achieve this. In this example I use the mount office. The UserBasicInfo component was mounted using the mount method, with the user object that I divers before. This method returns an object, which can exist used to check what was rendered and for interaction with the rendered component. This object is stored in a variable named wrapper.

And so on such an object we use the find method to cheque the content of the rendered component. In the outset test, nosotros cheque if the component has correctly rendered the user's first and concluding name in the advisable chemical element. In the second examination, we make sure that user details are non visible at get-go. And in the 3rd, whether a push button has been rendered to display user details.

In the second gear up of tests, we mountain the component again and immediately later on, we use simulate method to simulate a user pressing the button.

As a upshot, the state of our component is at present exactly every bit if the user had entered the page and clicked the push once. And so, in the commencement test, nosotros check whether the user details are displayed. In the second test, we check whether the text on the button has changed from 'Show user details' to 'Hibernate user details'.

More tests tin can be added, but for the purpose of showing the employ of Jest + Enzyme for a uncomplicated component it is more than enough.

Component with Redux

Complex projects written in React are very mutual. When an awarding consists not of several, but of dozens or even hundreds components, in that location is almost ever a problem with managing the state. Ane solution is to apply Redux, a predictable land container for JavaScript applications. In other words, Redux is an application data-flow compages, because it maintains the state of an application in a single immutable tree object. This object can't exist changed straight, simply using actions and reducers which create a new object.

Beneath you tin run across the implementation of the previous component adapted to work with Redux.

                              const                mapStateToProps                =                state                =>                ({                currentUser                :                state                .                currentUser                });                const                mapDispatchToProps                =                dispatch                =>                ({                updateEmail                :                e-mail                =>                dispatch                (                updateEmail                (                electronic mail                ))                });                const                UserInfoReduxComponent                =                ({                currentUser                ,                updateEmail                })                =>                {                const                [                showDetails                ,                setShowDetails                ]                =                useState                (                faux                );                const                renderUserDetails                =                ()                =>                (                <                div                className                =                {                styles                .                details                }                >                <                Typography                variant                =                "h5"                >Details</                Typography                >                <                p                >Login:                {                currentUser                .                login                }                </                p                >                <                p                >Age:                {                currentUser                .                historic period                }                </                p                >                <                TextField                blazon                =                "text"                label                =                "E-mail"                value                =                {                currentUser                .                email                }                onChange                =                {                changeEmail                }                />                </                div                >                )                const                getUserFullName                =                ()                =>                `                ${                currentUser                .                name                }                                                ${                currentUser                .                lastName                }                `                ;                const                toggleDetails                =                ()                =>                setShowDetails                (                !                showDetails                );                const                changeEmail                =                (                event                )                =>                updateEmail                (                outcome                .                target                .                value                );                return                // Return statement hasn't inverse                };                export                const                UserInfoRedux                =                connect                (                mapStateToProps                ,                mapDispatchToProps                )(                UserInfoReduxComponent                );                          

Just a few things have changed in comparision to the basic version of this component. New office mapStateToProps is responsible for mapping the Redux state to props of our component. The second office mapDispatchToProps is responsible for assigning Redux actions to component properties. With these actions the component is able to modify the state managed past Redux (in this case the component tin modify the user'southward e-mail using the updateEmail action).

Instead of read-simply text containing the user's email address, an editable Textfield has appeared. Now, when e-mail is changed, the updateEmail action is dispatched and email is changed in Redux shop.

The last change in the component implementation is the use of the connect role from Redux, thanks to which our component is connected to global application state.

However, for the component to be able to work with Redux, we demand to provide a Redux store in our application. And this is done as follows:

                              <                Provider                store                =                {                store                }                >                <                App                />                </                Provider                >                          

Now our component works with Redux, but our tests are unaware of this change. Running them ends up with the following mistake:

                              Error                :                Could                not                find                "                store                "                in                the                context                of                "                Connect(UserInfoReduxComponent)                "                .                          

This error means that we are trying to render a component with Enzyme that is now closely related to Redux, without providing any Redux context. The component has no access to the store.

Fortunately, the solution to this problem is very simple. The tests should be extended then that the component has access to the Redux Provider.

                              const                user                =                {...};                const                mockStore                =                configureStore                ([]);                const                store                =                mockStore                ({                currentUser                :                user                });                const                dispatchMock                =                ()                =>                Hope                .                resolve                ({});                store                .                acceleration                =                jest                .                fn                (                dispatchMock                );                describe                (                '                UserInfoRedux                '                ,                ()                =>                {                draw                (                '                After "Prove user details" button click                '                ,                ()                =>                {                const                wrapper                =                mount                (                <                Provider                shop                =                {                store                }                >                <                UserInfoRedux                />                </                Provider                >,                {                context                :                {                shop                }                }                );                wrapper                .                observe                (                Push                ).                simulate                (                '                click                '                );                test                (                '                should update user e-mail in store after input value alter                '                ,                ()                =>                {                // when                wrapper                .                find                (                '                input                '                ).                simulate                (                '                change                '                ,                {                target                :                {                value                :                '                new@email.com                '                }});                // then                wait                (                store                .                dispatch                ).                toHaveBeenCalledWith                (                {                electronic mail                :                "                new@email.com                "                ,                type                :                "                UPDATE_EMAIL                "                });                });                });                });                          

We have the new mockStore function created with configureStore from the redux-mock-store package, which is used to create a mocked Redux Shop containing user information in the currentUser field. Then we create a mock for the dispatch object, which is responsible for performing actions that change the Redux state.

The last step is to render the UserInfoRedux component inside the Redux Provider and pass our mocked store to the mount part.

Thanks to these changes, the tests laissez passer again.

At that place is also one new test that simulates irresolute an email address and checks if the changes were performed on the Redux state. This fashion, we can test whether user interactions are reflected in the application state stored in Redux.

Component with Router

Some other solution often constitute in larger projects that actually simplifies building applications is routing. React Router is responsible for routing in React applications. Thanks to it, we can, for example, utilise the same component at unlike addresses, in a slightly unlike way. As an instance, I used the aforementioned component that displays user information. At the /contour accost it displays the data of a currently logged user and allows to modify the email address. On the other paw, at the /users/{id} address the aforementioned component displays the user with given identifier, and it is read-just. Here are the changes in the component implementation that I made to achieve this result:

                              const                mapStateToProps                =                state                =>                ({                currentUser                :                state                .                currentUser                ,                users                :                state                .                users                });                const                mapDispatchToProps                =                acceleration                =>                ({                updateEmail                :                e-mail                =>                dispatch                (                updateEmail                (                email                ))                });                const                UserInfoReduxRouterComponent                =                ({                currentUser                ,                users                ,                updateEmail                ,                location                ,                match                })                =>                {                const                [                showDetails                ,                setShowDetails                ]                =                useState                (                false                );                const                renderUserDetails                =                ()                =>                (                <                div                className                =                {                styles                .                details                }                >                <                Typography                variant                =                "h5"                >Details</                Typography                >                <                p                >Login:                {                userData                .                login                }                </                p                >                <                p                >Age:                {                userData                .                age                }                </                p                >                <                TextField                type                =                "text"                label                =                "E-mail"                value                =                {                userData                .                email                }                onChange                =                {                changeEmail                }                disabled                =                {                location                .                pathname                !==                '                /profile                '                }                />                </                div                >                );                const                getUserFullName                =                ()                =>                `                ${                userData                .                name                }                                                ${                userData                .                lastName                }                `                ;                const                toggleDetails                =                ()                =>                setShowDetails                (                !                showDetails                );                const                changeEmail                =                (                outcome                )                =>                {                if                (                location                .                pathname                ===                '                /profile                '                )                {                updateEmail                (                outcome                .                target                .                value                );                }                };                const                getUserData                =                ()                =>                {                if                (                location                .                pathname                ===                '                /profile                '                )                {                return                currentUser                ;                }                else                {                const                foundUser                =                users                .                discover                ((                user                )                =>                user                .                id                ==                friction match                .                params                .                id                );                if                (                foundUser                )                {                render                foundUser                ;                }                }                return                nix                ;                };                const                userData                =                getUserData                ();                const                renderUserInfo                =                ()                =>                (                <>                <                Typography                variant                =                "h4"                >Info almost user:                {                getUserFullName                ()                }                (id:                {                userData                .                id                })</                Typography                >                <                p                >First proper name:                {                userData                .                name                }                </                p                >                <                p                >Concluding name:                {                userData                .                lastName                }                </                p                >                <                Button                style                =                {                {                '                border                '                :                '                1px solid gray                '                }                }                onClick                =                {                toggleDetails                }                >                {                showDetails                ?                '                Hide                '                :                '                Show                '                }                user details</                Button                >                {                showDetails                &&                renderUserDetails                ()                }                </>                );                render                (                <                Paper                foursquare                =                {                true                }                className                =                {                styles                .                paper                }                >                {                userData                &&                renderUserInfo                ()                }                {                !                userData                &&                <                h3                >User with given id does not be</                h3                >                }                </                Paper                >                );                };                export                const                UserInfoReduxRouter                =                connect                (                mapStateToProps                ,                mapDispatchToProps                )(                UserInfoReduxRouterComponent                );                          

So what has inverse? There is the new users property that comes from Redux store. This prop is the list of all existing users. There is the new disabled property on the email field. With this property, email is editable only at the /contour address. The nearly of import thing is how we ascertain the userData variable considering this variable is the source of information for rendering user data. Commencement, we define userData equally null. If the current pathname is /profile then nosotros assign a currently logged user to userData. If the accost is unlike, it means that nosotros are on the /users/{id} page. In this example, nosotros search for a user with a given identifier in a listing of all users. And then there are three possible results:

  • electric current user information is rendered at the /contour page,
  • a user with a given id is rendered at the /users/{id} folio,
  • text "User with given id does non be" is rendered if there is no user with a given id on the listing.

We need one more alter if we want our component to piece of work under both addresses. We accept to render this component inside BrowserRouter component, that comes from React Router, like this:

                              <                BrowserRouter                >                <                Switch                >                <                Route                exact                path                =                "/profile"                component                =                {                UserInfoReduxRouter                }                />                <                Route                verbal                path                =                "/users/:id"                component                =                {                UserInfoReduxRouter                }                />                </                Switch                >                </                BrowserRouter                >                          

The in a higher place code defines the routing of the application using the BrowserRouter component. We tell the router which component should be loaded for a given address. That'southward all.

Unfortunately, the tests stopped working again, and hither'southward the fault nosotros get afterwards running them:

                              Error                :                Uncaught                [                TypeError                :                Cannot                read                property                '                pathname                '                of                undefined                ]                          

This error ways that during the test execution, the component does not have access to the location object from which the pathname attribute is retrieved. Equally mentioned to a higher place, this object is provided by React Router at runtime. In tests, however, we have to provide information technology differently. Here's the snippet of the exam code that is responsible for information technology:

                              const                path                =                `/contour`                ;                const                friction match                =                {                isExact                :                true                ,                path                ,                url                :                path                };                const                location                =                createLocation                (                match                .                url                );                const                store                =                getStore                ();                const                wrapper                =                mountain                (                <                Provider                store                =                {                shop                }                >                <                UserInfoReduxRouter                match                =                {                match                }                location                =                {                location                }                />                </                Provider                >,                {                context                :                {                store                }                }                );                          

First, we define the address at which we want to test our component. Adjacent, nosotros create a mock for the match object and pass the accost to information technology (the path variable). Using the createLocation function that comes from the history packet, we create a mock for the location object. Finally, we pass the created mocks of lucifer and location objects to the component props. Thanks to these changes, we provided the routing context and the component can work properly. Tests pass once again.

Component with TypeScript

The terminal tool I would like to mention is the TypeScript language. Imagine a huge React project with 20 developers and hundreds of components. Each component accepts several props. Such a project written in JS, without typing, would be very difficult to maintain. Programmers have to guess variable types. Is it, a string? or maybe a number? What fields must be defined in the user property for the component to work properly? That is why TypeScript was created, it is a linguistic communication that is a superset of JavaScript, and it introduces static type checking. Looking at the implementation of the component above, nosotros can see what props the component takes. This is, for example, currentUser, but nosotros practice non know what backdrop should such object provide. Is e-mail required? Can nosotros skip historic period? TypeScript solves this kind of problems.

Typescript as well makes writing and maintaining tests easier, because:

  • nosotros don't have to wait at the component implementation every few seconds merely to verify its API thanks to static types,
  • when some public interface is changed and it was previously used in some test, we don't demand to run the tests to know which of them fails - they merely won't compile,
  • we do not demand to write examination cases where we are passing wrong types to tested part or component - Typescript makes certain that there are no such situations in code.

Now let'due south see how our component looks like, written in TypeScript.

First, nosotros define the interface that describes a user:

                              export                interface                User                {                id                :                number                ;                name                :                string                ;                lastName                :                string                ;                login                :                string                ;                email                :                string                ;                age                ?:                number                ;                }                          

The interface clearly defines which fields describe a user object and which fields are not required. In this case, the age field is optional.

Here is the use of TypeScript in the component itself:

                              const                mapStateToProps                =                (                state                :                {                currentUser                :                User                ;                users                :                User                [];                })                =>                ({                currentUser                :                state                .                currentUser                ,                users                :                state                .                users                });                const                mapDispatchToProps                =                (                dispatch                :                Dispatch                )                =>                ({                updateEmail                :                (                electronic mail                :                cord                )                =>                dispatch                (                updateEmail                (                email                ))                });                consign                interface                UserInfoReduxRouterTsProps                {                currentUser                :                User                ;                users                :                User                [];                updateEmail                :                (                email                :                string                )                =>                UpdateEmailAction                ;                location                :                Location                ;                }                const                UserInfoReduxRouterTsComponent                :                React                .                FC                <                UserInfoReduxRouterTsProps                >                =                (                {                currentUser                ,                users                ,                updateEmail                ,                location                }                )                =>                {                const                [                showDetails                ,                setShowDetails                ]                =                useState                (                false                );                const                renderUserDetails                =                ():                JSX                .                Chemical element                =>                (                <                div                className                =                {                styles                .                details                }                >                <                Typography                variant                =                "h5"                >Details</                Typography                >                <                p                >Login:                {                userData                .                login                }                </                p                >                <                p                >Age:                {                userData                .                age                }                </                p                >                <                TextField                fullWidth                blazon                =                "text"                label                =                "E-mail"                value                =                {                userData                .                electronic mail                }                onChange                =                {                changeEmail                }                disabled                =                {                location                .                pathname                !==                '                /profile                '                }                />                </                div                >                );                const                getUserFullName                =                ():                JSX                .                Element                =>                `                ${                userData                .                name                }                                                ${                userData                .                lastName                }                `                ;                const                toggleDetails                =                ():                void                =>                setShowDetails                (                !                showDetails                );                const                changeEmail                =                (                issue                :                React                .                ChangeEvent                <                any                >                ):                void                =>                {                if                (                location                .                pathname                ===                '                /profile                '                )                {                updateEmail                (                result                .                target                .                value                );                }                }                const                getUserId                =                ():                number                =>                parseInt                (                location                .                pathname                .                separate                (                '                users/                '                )[                1                ],                10                );                const                getUserData                =                ():                User                =>                {                if                (                location                .                pathname                ===                '                /contour                '                )                {                render                currentUser                ;                }                else                {                const                foundUser                =                users                .                find                ((                user                :                User                )                =>                user                .                id                ==                getUserId                ());                if                (                foundUser                )                {                return                foundUser                ;                }                }                return                aught                ;                };                const                userData                :                User                =                getUserData                ();                // No more than changes, rest is the same.                };                export                const                UserInfoReduxRouterTs                =                withRouter                (                connect                (                mapStateToProps                ,                mapDispatchToProps                )(                UserInfoReduxRouterTsComponent                ));                          

The component has inverse a lot. The well-nigh important affair is the definition of the UserInfoReduxRouterTsProps interface, which describes what props the component takes, which are optional and what types they have. Equally a result, the programmer who wants to use such a component knows immediately what to pass to it. Other changes are the appearance of types for parameters in functions as well every bit functions return types. Both are very helpful for future changes in component or code refactoring.

After these changes tests stopped working over again. The error is:

                              Type                '                Location<{}>                '                is                missing                the                following                properties                from                type                '                Location                '                :                ancestorOrigins                ,                host                ,                hostname                ,                hef                ,                and                half dozen                more                .                          

And so far in tests, we passed the mocked location object directly as component props. Now TypeScript detects that the type of passed location object is not identical to the type expected by the component. To set this issue we can simply simulate a context similar to the one in which the component lives while the awarding is running. To do this, we accept to embed the component in the context of the router. With this change the location parameter comes from Router, and it has exactly the type that the components expect.

                              const                path                =                `/profile`                ;                const                store                =                getStore                ();                const                wrapper                =                mount                (                <                MemoryRouter                initialEntries                =                {                [                path                ]                }                >                <                Provider                store                =                {                shop                }                >                <                UserInfoReduxRouterTs                />                </                Provider                >                </                MemoryRouter                >,                {                context                :                {                store                }                }                );                          

The MemoryRouter component is imported from the 'react-router' package. We define a variable path to simulate the beliefs of the component at a specific accost (here /profile). And then, we mountain our component wrapped in MemoryRouter, and pass the previously defined path as prop.

Thanks to these changes, the tests pass again.

Conclusion

As I mentioned at the outset, it's hard to find frontend applications that are created without the use of frameworks these days. As can exist seen in this text, it is besides hard to create complex projects without introducing additional libraries like Router, Redux or TypeScript. Yous can also find that including them to the projection requires some changes to the tests likewise. However, if nosotros take tests seriously from the very beginning of application development, and we create them regularly, we can quickly and efficiently adapt them to changes in the application.

But would it be easy if we wrote tests subsequently the application is already developed and uses all these extensions? Probably not, often if we do not start testing the application from the get-go, then the price of introducing the tests is so high, that nosotros make up one's mind to give them up, and our application loses a lot of value, because without tests it is difficult to maintain the app and introduce farther changes.

So, I encourage yous to test!

If yous are interested in the implementation details take a expect at the testing-react-components github repository.