Testing a React Application with React Hooks with Jest and Enzyme for newbies

profilsoftware

Profil Software

Posted on March 2, 2023

Testing a React Application with React Hooks with Jest and Enzyme for newbies

Introduction

ReactJS application developmentleads to code base growth. Once a certain point is reached, an application becomes more complex. Tasks start to get more and more time consuming. Adding a new feature or even fixing a bug creates a real risk for your application. If there is no testing at this point, it should be introduced immediately. Testing makes an app more robust and less prone to error. It’s the best way to ensure that your code works as expected in every possible scenario.

There is nothing more frustrating than spotting an error that could have been found and removed during development if only tests had been written. Everyone wants to create bug-free applications that users will love. To achieve this goal, every front-end developer has to ensure that at least crucial parts of their application are tested and the risk of introducing badly working code is minimal.

My goal is to share some practical knowledge about testing React applications using Jest and Enzyme. It comes in handy when you're working for any React Native Agency as well.

This article contains code fragments from the following repository: https://github.com/dybik08/github-browser.

Quick Overview

Modern React applications are made by stateless functional components with hooks. It is the direction React is evolving towards. In this article I won’t focus on testing class-based components which are displaced by functional ones.

We’ll learn how to test them starting with UI testing. After that we’ll move to React Hooks testing, then we’ll move to Redux Thunk asynchronous actions, and we’ll finish with Redux.

For the purpose of this article, I’ve made a small application that displays searched GitHub repository info. It uses Github v3 public API. For more info check the Github API docs here. Project structure can be found in the image below. The project was bootstrapped by React CLI which handled installation of all libraries and created a boilerplate project with a bunch of useful commands ready to use like npm run start or npm run test.

Getting back to the project, users can search Github repositories by filling in the input and pressing the search button or enter key on the keyboard. Users can then press the details button to see detailed info of the selected repository or check other repositories of the selected repository owner.
Project structure:

Image description

Enzyme

Enzyme is a JavaScript Testing utility built for React that makes it easier to test your React Components’ output. Enzyme’s main role is to prepare React components for testing by mount, shallow, or render methods and to provide various methods for selecting desired components. It also provides a series of APIs used to examine the component’s properties. For more details please follow this link to check the Enzyme docs directly. Enzyme needs a testing library to provide a foundation for the tests it generates. It’s commonly used along with Jest.

Jest

Jest is a fully-featured testing framework. It provides a test runner and provides dozens of methods that can be used in testing along with an assertion library. Follow this link to check the assertion library API directly in Jest docs. Jest combined with Enzyme works great for React, but it’s not limited just to React. It’s very popular and widely used in the JavaScript ecosystem. For web applications built entirely in vanilla JavaScript, Jest allows you to test the whole application with one test library.

Enjoying this article? Need help? Talk to us!
Receive a free consultation on your software project

Enzyme render methods — what do they do and what are they for?

Mount — Ideal for use cases when you have components that may interact with the DOM API or use React lifecycle methods in order to fully test the component. This is the default method when writing component tests.

Shallow — Renders only a single component, not including its children. It is useful to isolate the component for pure unit testing. The biggest advantage is that it protects against changes or bugs in a child component.

A summary of both methods can be found in the table below.

Image description

Testing stateful functional components — useState hook

Mocking the useState hook

To properly test stateful functional components, it would be good to start with mocking the useState hook along with the setState function. The SearchReposInput component’s purpose is to handle text entered by the user and pass it to the Redux action on button press.

The image below shows how to handle mocking. The Jest.fn() method in the setState function will be needed to ensure setState was called with the correct arguments or the correct number of times. There is an option to set the default useState value, but in this case it will be left untouched.

    const setState = jest.fn();
    const useStateSpy = jest.spyOn(React, 'useState');
    useStateSpy.mockImplementation(initialState => [initialState, setState]);
Enter fullscreen mode Exit fullscreen mode

Once the mock is correctly set up, it is time to write some tests. For this component, testing should start by finding an input. It can be done by CSS class name. It’s also a good practice to find input or any other interactive element by value seen by the user like placeholder text, button name, etc. An example of the test can be found in the image below.

 it('should update state on input change', () => {
        const newInputValue = 'React is Awesome';
        wrapper.find('.ant-input').simulate('change', { target: { value: newInputValue } });
        expect(setState).toHaveBeenCalledWith(newInputValue);
    });
Enter fullscreen mode Exit fullscreen mode

Mocking Redux hooks

The code below, taken from SearchReposInput.test, also shows how to mock the React-Redux useSelector and useDispatch hooks. When writing tests for components using the Redux dispatch method to dispatch actions, there should be a spy assigned to the method.

It can be done the same way as you see in the image below. It will provide an option to check which function is dispatched along with its arguments. Tests that run on Redux’s store values need to return a mock value from the useSelector method. It provides better control over the tested component.

const mockDispatch = jest.fn();
jest.mock('react-redux', () => ({
    useSelector: jest.fn().mockReturnValue({
        // useSelector will return redux state object
        repositories: {
            loading: false,
            repos: [],
        },
    }),
    useDispatch: () => mockDispatch,
}));

Enter fullscreen mode Exit fullscreen mode

The code below, also taken from SearchReposInput.test, shows how to mock functions from external modules. It can be useful when a test needs specific conditions like a mocking response from a function calling an external API. In this example, along with Jest’s spy on a mocked function, it was necessary to mock function output. Mocked values should be close to the original response to make the testing case more realistic. This was achieved by using mockReturnedValue. When the expected response is a promise, the mockResolvedValue method should be used instead.

jest.mock('../../actions/networkActions', () => ({
    fetchRepos: jest.fn().mockResolvedValue({
        data: {
            name: 'name',
            description: 'some description',
            language: 'javascript',
            stargazers_count: 1,
            forks: 2,
            owner: {
                repos_url: '',
                type: '',
                avatar_url: '',
                login: '',
            },
            created_at: '2020:10:12',
            updated_at: '2020:10:14',
            license: {
                name: 'MIT',
            },
        },
    }),
}));
Enter fullscreen mode Exit fullscreen mode

Testing logic inside the useEffect hook

One of the most common use cases for the useEffect hook is to execute API calls after component mounts. A good testing example of this feature is fetching data and further data processing in useEffect.

Once the user presses the details button on one of the repositories in the repositories list, a modal opens, displaying the selected repository details. One of the sections displays other repositories linked to the selected repository owner. Those “extra” repositories are fetched after the modal mounts, to be ready when users press the additional user repositories dropdown panel. To test if components behave as expected, some preparations are required. First, mock the fetchAdditionalUserRepos function from the networkActions module along with its response, as shown in the image below.

const mockFetchAdditionalUserReposResponse = ['repo1', 'repo2', 'repo3'];
const mockFetchAdditionalUserRepos = jest.fn().mockResolvedValue(mockFetchAdditionalUserReposResponse);
networkActions.fetchAdditionalUserRepos = mockFetchAdditionalUserRepos;
Enter fullscreen mode Exit fullscreen mode

Since it's a promise, the mock will be handled by the mockResolved function.
With everything prepared, we can write a test to ensure extra data is correctly fetched after the component mounts. The test code is presented in the image below.

it('fetch additional user repos on mount', () => {      expect(mockFetchAdditionalUserRepos).toHaveBeenCalledWith('repos_url');     expect(setState).toHaveBeenCalledWith(mockFetchAdditionalUserReposResponse);
    });
Enter fullscreen mode Exit fullscreen mode

Redux Thunk — testing async actions is easy!
In this section, I’d like to focus on testing asynchronous actions. Those actions are handled by Redux thunk middleware. Testing them is more complicated, especially for the first time. I’ll also show you how to mock the axios library to return resolved or rejected values that are needed.

The main goal is to ensure that all expected actions are correctly executed when the fetchRepos function is called.

Testing preparation starts by mocking a Redux store via the configureMockStore function from Redux-mock-store library. An example of a Redux store mock is shown below.

const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
Enter fullscreen mode Exit fullscreen mode

Next, we have to mock the axios library to imitate the correct network call. Since only the get method from the axios module is needed for this test, only this method will be mocked. The image below presents a mock get method from the axios library.

const mockedGet = jest.fn().mockResolvedValue({ data: { items: [mockedRepository, mockedRepository] } });
        axios.get = mockedGet;
Enter fullscreen mode Exit fullscreen mode

The next step is to prepare a variable with expected actions and their payload. The image below displays how the expected actions array should look for the given example — fetchRepos test.

const expectedActions = [
            {
                type: NetworkActionNames.START_FETCHING_REPOS,
            },
            { type: NetworkActionNames.FETCHING_REPOS_DONE, payload: [mockedRepository, mockedRepository] },
        ];
Enter fullscreen mode Exit fullscreen mode

Once all mocks are prepared, it is time for a thunk action test. This test will check if all asynchronous actions were called in the correct order and carried an expected payload. It will also ensure that the axios get method was called before any Redux action. Example code for the thunk action is displayed in the image below.

const store = mockStore([]);
        return store.dispatch(fetchRepos('React')).then(() => {
            expect(mockedGet).toHaveBeenCalled();

            expect(store.getActions()).toEqual(expectedActions);
        });
Enter fullscreen mode Exit fullscreen mode

Redux Testing — make your app state great again (and bug-free, of course)

This section will focus on the reducers testing. Every Redux testing scenario should start with an initial state check. The image below displays an example test for checking the correct value of the reducer's initial state. Example code for an initial state test is shown in the image below.

it('should return initial state', () => {
        expect(repositories(undefined, {})).toEqual(repositoriesReducerInitialState);
    });
Enter fullscreen mode Exit fullscreen mode

Reducer tests for all action cases are mandatory to ensure that every Redux action listener acts as expected and the action payload is correctly handled. A test’s job is to check if the returned, updated state value is the same as the one saved in the expected state variable. Those kinds of tests are very straightforward and help maintain app state management. An example of the reducer test code is shown in the image below.

it('should handle START_FETCHING_REPOS action', () => {
        expect(
            repositories(repositoriesReducerInitialState, {
                type: NetworkActionNames.START_FETCHING_REPOS,
                payload: undefined,
            })
        ).toEqual({
            ...repositoriesReducerInitialState,
            loading: true,
        });
    });
Enter fullscreen mode Exit fullscreen mode

Interested in working for our software development company in Poland?
Check out our current job offers now

Snapshot testing — should we even care about it?

Snapshot tests compare the JSON image of the rendered component with the JSON image created during the previous tests.

As opposed to end-to-end tests, snapshot tests are run in a command line runner rather than a real browser. It is a good way to ensure our component looks exactly as we want. It’s useful especially when the look changes dynamically depending on passed props.

Even if they’re not popular among the React community and often skipped, it’s good to know what they are, what they do, and of course how to write one.

The image below displays code for a snapshot test. The renderer creates the method that returns a component snapshot which can be further transformed to a JSON object. The JSON object format is required by the toMatchSnapshot function which, under the hood, compares the current component tree to the historical snapshot saved in the snapshot file. An example of a snapshot file can be found here.

it('renders correctly', () => {
        const tree = renderer.create(wrapper).toJSON();
        expect(tree).toMatchSnapshot();
    });

Enter fullscreen mode Exit fullscreen mode

Conclusion — receiving another bug report makes you mad? Consider testing!

This knowledge should help you start code testing using React and Redux. With your code test covered, you will get rid of struggles with the same bugs which reoccur from time to time. If you’ve ever been in a situation where you’re nervous to add new code to an existing codebase in the fear that you’ll break something in a completely different part of the code, make your and your teammates’ lives easier by diving into the testing world! When developing an application it's important to include QA testing services as well.

Need to check something directly in the source code? Visit: https://github.com/dybik08/github-browser

If you'd like to get more tips on programming and how to be a better programmer, our software development blog features many subjects that may interest you. Learn how to improve your coding skills, how to Streamline Your Wagtail CMS Development, and many more.

💖 💪 🙅 🚩
profilsoftware
Profil Software

Posted on March 2, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related