Pages

Testing react components using Hooks and Mocks

 How to test React components using Hooks & Mocks?

Before we start the detailed tutorial on how to test React components using hooks and mocks, it is important to get familiar with some of the basic terms that we will use in this article.


  1. Jest: 

It is a testing framework for Javascript that is supported by Meta and built on top of Jasmine. Jest was created to guarantee the accuracy of the JavaScript codebase. Jest can gather data on code coverage from entire projects, including untested files and in this article we will use it to test functions, react hooks, and API requests by simulating them.


  1. Enzyme

Enzyme is a JavaScript Testing utility for React that simplifies asserting, manipulating, and traversing the output of your React Components. It was initially developed by Airbnb before being transferred to a separate organization. It works with all major test runners and assertion libraries. So, you are free to choose mocha or chai as your test runner.


  1. React-testing-library

React Testing Library is a library for testing React components. It is designed to simplify testing stateful React components as well as provide helpful utility functions for testing your application. It gives you a set of APIs that let you search for elements inside a component, start events, and get feedback on the results. 


The main difference between Enzyme and React Testing Library (RTL) is that Enzyme is a testing utility for React that makes it easier to assert, manipulate, and traverse your React components' output. It is a library with more features that lets React apps be tested in more detail. On the other hand, React Testing Library (RTL) is a library built on top of the DOM Testing Library that provides custom React-specific assertions and queries. It is a more lightweight library that focuses on testing your components from the user's perspective, rather than testing implementation details.


As you are now familiar with these terms, you will understand them easily in this article. 


Step 1: Set up your testing environment

Start by setting up your testing environment with a test runner, an assertion library, and any other necessary tools such as a mocking library.


As a common template of a React app, we will use create-react-app which has the Jest library installed under the hood.


npx create-react-app my-app


 You can either use a shallow renderer or mount the component with Enzyme. This will allow you to have access to the component instance.



npm i --save-dev enzyme @wojtekmaj/enzyme-adapter-react


Now we have to go to src/setupTests.js. to set the next configuration:


import { configure } from 'enzyme';

import Adapter       from '@wojtekmaj/enzyme-adapter-react-17';

 

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


create-react-app uses react-testing-library, so the enzyme should be installed separately.


Testing React components with Mocks.

Mocking is an important part of writing tests for React because it lets you get rid of dependencies on things like remote requests, modules, and hook logic. I used a module that retrieves and displays a user's profile as an illustration.


Let's start writing a test suite. This should include tests for each of the component’s props and states, as well as any interactions with the component.


Using App.js

import Profile from './components/Profile';

import Loader  from './components/Loader';

import ErrorHandler from './components/ErrorHandler';

import useFetch from './hooks/useFetch';

 

function App() {

   const { response, error, loading, fetchData } = useFetch('https://randomuser.me/api');

 

   const handleFetch = () => {

       if (!loading) {

           fetchData();

       }

   };

 

   return (

       <div classname="App">

           {response ? <profile data="{response.results[0]}"> : null}

           {loading ? <loader> : null}

           {error ? <errorhandler error="{error}"> : null}

           <button onclick="{handleFetch}">Fetch</button>

       </errorhandler></loader></profile></div>

   );

}


Using useFetch.js:

You can also use useFetch.js to do this.


import React from 'react';

import api from '../singleton/api';

 

const useFetch = (url, options) => {

   const [ response, setResponse ] = React.useState(null);

   const [ error, setError ] = React.useState(null);

   const [ loading, setLoading ] = React.useState(false);

 

   const fetchData = async () => {

       try {

           setLoading(true);

           const res = await api.apiClient.get(url, options);

 

           setResponse(res);

       } catch (err) {

           setError(err);

       } finally {

           setLoading(false);

       }

   };

 

   React.useEffect(() => {

       fetchData();

   }, []);

 

   return { response, error, loading, fetchData };

};


Coders can test react components using any of the methods listed above. In this article we will use useFetch.js because It’s better to move from the bottom to the top. And also it is a good chance to show how to test react functions with hooks. So, we will test everything App.js uses and then test App.js itself.


import React       from 'react';

import mockUser    from '../__mocks__/user'; // 1

import api         from '../singleton/api';

 

jest.mock('../singleton/api', () => ({ apiClient: { get: jest.fn() } }));

 

describe('Test hook useFetch', () => {

   const setResponse = jest.fn(); // 2

   const setError = jest.fn();

   const setLoading = jest.fn();

 

   // useState indexes

   const responseIndex = 0;

   const errorIndex = 1;

   const loadingIndex = 2;

   const useStateMocks = {

       [responseIndex] : [ null, setResponse ],

       [errorIndex]    : [ null, setError ],

       [loadingIndex]  : [ false, setLoading ]

   };

 

   beforeEach(() => {

       api.apiClient.get.mockImplementation(() => Promise.resolve(mockUser)); // 3

 

       let useStateIndex = 0;

 

       jest.spyOn(React, 'useState').mockImplementation(() => { // 4

           const useStateMock = useStateMocks[useStateIndex];

 

           useStateIndex++;

 

           return useStateMock;

       });

 

       // We should mock it because useEffect is not supported by Enzyme's shallow rendering.

       jest.spyOn(React, 'useEffect').mockImplementation(f => f()); // 5

   });

 

   ...

});


We imported an object with a user, created mock functions to understand how many times hook will be called, and a mock function in the above code. 


Now, we will create a mock of usefetch to test components in App.js.


import useFetch from './hooks/useFetch';

import mockUser from './__mocks__/user';

 

const mockFetchData = jest.fn();

const mockError = new Error('User is not found');

 

jest.mock('./hooks/useFetch.js', () => jest.fn()); // 1

 

describe('Test App.js', () => {

   beforeEach(() => {

       useFetch.mockImplementation(() => ({ // 2

           response  : null,

           error     : null,

           loading   : false,

           fetchData : mockFetchData

       }));

   });

 

   it('Should render a Profile component', () => {

       useFetch.mockImplementation(() => ({ response: mockUser })); // 3

      

       ...

   });

 

});


Testing React components with Enzyme.

Using enzymes, we can test React components in three ways.


Shallow rendering method

The shallow rendering method is used when React developers need to test their component as an independent unit. Child elements in your component are not rendered in this method.


Static Rendering

This method generates HTML output for your React component. It appears to be a strategy very similar to RTL's, but with restrictions on item selection. Enzyme uses the Cheerio third-party library along with the core jQuery lean implementation to parse and work with HTML.


Full rendering using Mount

This technique is typically used to test a HOC-wrapped component or in scenarios where you have components that might communicate with DOM APIs. It renders every element that component's child components have.


Testing React with hooks using Enzyme.


  1.  useFetch.test.js


it('Should fetch a value', async () => {

       shallow(<testusefetchhooks>);

 

       await new Promise(process.nextTick); // 1

 

       expect(setLoading).toHaveBeenCalledTimes(2);

       expect(setLoading).toHaveBeenCalledWith(true);

       expect(setLoading).toHaveBeenCalledWith(false);

 

       expect(setResponse).toHaveBeenCalledTimes(1);

       expect(setResponse).toHaveBeenCalledWith(mockUser);

   });

 

   it('Should throw an error during fetching', async () => {

       const mockError = new Error('User is not found');

 

       api.apiClient.get.mockImplementation(() => Promise.reject(mockError));

 

       shallow(<testusefetchhooks>);

 

       await new Promise(process.nextTick);

 

       expect(api.apiClient.get).rejects.toThrow(mockError);

 

       expect(setLoading).toHaveBeenCalledTimes(2);

       expect(setLoading).toHaveBeenCalledWith(true);

       expect(setLoading).toHaveBeenCalledWith(false);

 

       expect(setError).toHaveBeenCalledTimes(1);

       expect(setError).toHaveBeenCalledWith(mockError);

   });


The above coding is done to make Jest wait till all asynchronous code is executed.


  1.  App.test.js


it('Should render a Profile component', () => {

       useFetch.mockImplementation(() => ({ response: mockUser }));

       const wrapper = shallow(<app>);

 

       expect(wrapper.find(Profile)).toHaveLength(1);

       expect(wrapper.find(Profile).props().data).toBe(mockUser.results[0]);

   });

 

   it('Should trigger fetchData if loading is false', () => {

       const wrapper = shallow(<app>);

 

       wrapper.find('button').simulate('click');

 

       expect(mockFetchData).toHaveBeenCalledTimes(1);

   });

 

   it('Shouldn\'t trigger fetchData if loading is true', () => {

       useFetch.mockImplementation(() => ({ loading: true }));

       const wrapper = shallow(<app>);

 

       wrapper.find('button').simulate('click');

 

       expect(mockFetchData).toHaveBeenCalledTimes(0);

   });

 

   it('Should render a Loading component', () => {

       useFetch.mockImplementation(() => ({ loading: true }));

       const wrapper = shallow(<app>);

 

       expect(wrapper.find(Loader)).toHaveLength(1);

   });

 

   it('Should render a ErrorHandler component', () => {

       useFetch.mockImplementation(() => ({ error: mockError }));

       const wrapper = shallow(<app>);

 

       expect(wrapper.find(ErrorHandler)).toHaveLength(1);

       expect(wrapper.find(ErrorHandler).props().error).toBe(mockError);

   });


In the above code, we have used a shallow method of Enzyme was used to achieve 100% coverage. There is another way to test react components with hooks. I believe the greatest benefit of Enzyme is that tests written with simple language accurately represent what a component should accomplish.


Testing with React Testing Library

React hooks can be tested in a number of ways, depending on the testing library. This section demonstrates how to test hooks using the react-testing-library tools. We used the testing library react hooks @testing-library/react-hooks to test a hook because it was created by the same team that created react-testing-library and it demonstrates a slightly different method for testing react hooks.



  1. useFetch.js


import mockUser    from '../__mocks__/user';

import useFetch    from './useFetch';

import { renderHook, act } from '@testing-library/react-hooks';

import api from '../singleton/api';

 

jest.mock('../singleton/api', () => ({ apiClient: { get: jest.fn() } }));

 

describe('Test hook useFetch', () => {

   beforeEach(() => {

       api.apiClient.get.mockImplementation(() => Promise.resolve(mockUser));

   });

 

   it('Should fetch a value', async  () => {

       const { result } = renderHook(() => useFetch());

 

       await act(async () => {

           await result.current.fetchData()

       });

 

       expect(result.current.response).toBe(mockUser);

   });

 

   it('Should throw an error during fetching', async () => {

       const mockError = new Error('User is not found');

 

       api.apiClient.get.mockImplementation(() => Promise.reject(mockError));

 

       const { result } = renderHook(() => useFetch());

 

       await act(async () => {

           await result.current.fetchData()

       });

      

       expect(result.current.error).toBe(mockError);

   });

});



  1.  App.test.js





it('Should render a Profile component', async () => {

       useFetch.mockImplementation(() => ({ response: mockUser }));

       render(<app>);

       const { title, first, last } = mockUser.results[0].name;

       const name = `${title} ${first} ${last}`;

       const img = await screen.findByAltText(name);

 

       expect(await screen.findByText(name)).toBeInTheDocument();

       expect(img.src).toBe( mockUser.results[0].picture.large);

   });

   it('Should trigger fetchData if loading is false', () => {

       render(<app>);

       fireEvent.click(screen.getByText('Fetch'));

       expect(mockFetchData).toHaveBeenCalledTimes(1);

   });

   it('Shouldn\'t trigger fetchData if loading is true', () => {

       useFetch.mockImplementation(() => ({ loading: true }));

       render(<app>);

       fireEvent.click(screen.getByText('Fetch'));

 

       expect(mockFetchData).toHaveBeenCalledTimes(0);

   });

 

   it('Should render a Loading component', async () => {

       useFetch.mockImplementation(() => ({ loading: true }));

       render(<app>);

       expect(await screen.findByText('Loading...')).toBeInTheDocument();

   });

 

   it('Should render a ErrorHandler component', async () => {

       useFetch.mockImplementation(() => ({ error: mockError }));

       render(<app>);

       expect(await screen.findByText(mockError.message)).toBeInTheDocument();

   });


Final Thoughts: 

Testing React components using Hooks and Mocks is an effective way to ensure that your code works as expected. Hooks provide a powerful and flexible way to test components with less code, while mocks provide an efficient way to mock out the behavior of components. Together, these two tools can help you quickly and easily test your components.


No comments:

Post a Comment

Make new Model/Controller/Migration in Laravel

  In this article, we have included steps to create a model and controller or resource controller with the help of command line(CLI). Here w...