Blog / React

How to test custom react hooks

How to test custom react hooks

In this post you will learn everything you need to be able know how to test custom react hooks with confidence.

Will MaygerWill Mayger
July 11, 2021
Article

I’m sure we have all been there when starting to test custom React hooks.

How do we best test custom react hooks? Why can’t we just render the hook by itself?

Well in this post we will be answering those questions and more.

React hooks have been around since version 16.8 of React, and they have had a huge impact (for the better in my opinion).

The thing that has not always been so clear though is how we can go about testing our own custom hooks.

In this post we are going to aim to cover everything about testing custom react hooks that you need to know.

As per usual with all of my blog posts, I will aim to explain how to test custom react hooks without any technical jargon so everyone can understand and get testing.

In this post we will be focusing on using the react-hooks testing-library and the react testing-library to create unit tests with jest.

How to test custom react hooks

How to test custom react hooks graphic

The easiest way to test a custom react hook is to make use of the @testing-library/react-hooks library so you can test your react hooks in a similar way to a react component by passing the hook into the renderHook function and then asserting if the results are correct, similar to how you would in normal unit tests.

Here is a quick example of how this looks when using the react-hooks testing-library to test custom react hooks:

A basic example of a custom react hook:

import { useState } from 'react';
export const defaultMessage = 'default example message';

export default function useExampleCustomReactHook() {
  const [message, setMessage] = useState(defaultMessage);
  return {
    message,
    setMessage,
  };
}

A basic test using @testing-library/react-hooks and renderHook:

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

describe('useExampleCustomReactHook', () => {
  it('Should provide a default message', () => {
    const { result } = renderHook(useExampleCustomReactHook);
    expect(result.current.message).toEqual(defaultMessage);
  });

  it('Should update the message', () => {
    const updatedMessage = 'hello world!';
    const { result } = renderHook(useExampleCustomReactHook);
    expect(result.current.message).toEqual(defaultMessage);

    act(() => {
      result.current.setMessage(updatedMessage);
    });

    expect(result.current.message).toEqual(updatedMessage);
  });
});

As you can see, the renderHook function from @testing-library/react-hooks is returning an object with a field called result.

We can then use that field like we would use a ref in React to be able to get the return values from our custom react hook.

result.current.message;
result.current.setMessage('');

So, why is the react-hooks testing-library and renderHook function needed here? Afterall a react hook does seem to look like an ordinary function.

Unfortunately (and fortunately), react hooks do more work than ordinary functions and have to still be able to work in the lifecycle of React with other hooks like useEffect or useState, and in order for these to work we need to make sure we are using the function properly.

Because of this, we have to use some kind of render method that will render the hook using React when testing.

However, if a function has no reference to a hook or has no need for the react lifecycle then it probably isn’t going to be a react hook, and if it isn’t a hook then you could just run the function and assert it in your tests the normal way you would in a unit-test.

Here is what it looks like when you try to test a react hook like a normal function in a jest unit test:

Error when testing a custom react hook as a function
  Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
  1. You might have mismatching versions of React and the renderer (such as React DOM)
  2. You might be breaking the Rules of Hooks
  3. You might have more than one copy of React in the same app
  See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.

And this is why we need something like react-hooks testing-library which lets us attach our hook into the react lifecycle flow to be able test it as it would behave in a react component.

How to test custom react hooks with providers

Now we have covered the base scenario of how to test custom react hooks and why, we now need to look into how we can apply providers to our hooks.

A provider can be any kind of context provider, so this could be react-redux, a custom React context,react-router, network providers such as apollo client and more.

All providers will have a route component that shares a context to child react components.

When you are testing react components that make use of these providers and contexts, you must also include the providers when testing.

Without adding in these providers, your components could throw errors when trying to render because they won’t be able to find the contexts which will then cause your tests to fail.

When testing with react components it is easy enough just to add in the providers into the render function of your unit test, for example:

import { render } from '@testing-library/react';
import ExampleProvider from './ExampleProvider';
import ExampleComponent from './ExampleComponent';

describe('ExampleComponent', () => {
  it('Should render', () => {
    const { getByText } = render(
      <ExampleProvider>
        <ExampleComponent />
      </ExampleProvider>
    );
    expect(getByText('Hello World')).toBeInTheDocument();
  });
});

In this example we are wrapping our ExampleComponent with our ExampleProvider which contains a custom context, but this could just as easily be redux, or apollo client, and so on.

So how do we do this when we use the react-hooks testing-library?

The react-hook testing-library lets you pass in a react hook directly to the renderHook method, so you cannot add in the providers because it only accepts a react hook function and not JSX.

However, the react-hooks testing-library function, renderHook, comes with additional options that you can provide when rendering a custom react hook and this is where we can add in these providers.

The function accepts a second argument that lets you pass in an optional wrapper for your hook.

A wrapper can be any kind of component you like, as long as it accepts children as a prop and renders them.

This means that all you need to do is to create a wrapper that contains all the providers you need and then to pass it into the hook.

Using our example from before with the custom react hook, let’s see how that will look:

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

const Wrapper = ({ children }) => (
  <ExampleProvider>
    {children}
  </ExampleProvider>
)

describe('useExampleCustomReactHook', () => {
  it('Should provide a default message', () => {
    const { result } = renderHook(
      useExampleCustomReactHook,
      { wrapper: Wrapper },
    );
    expect(result.current.message).toEqual(defaultMessage);
  });

  it('Should update the message', () => {
    const updatedMessage = 'hello world!';
    const { result } = renderHook(
      useExampleCustomReactHook,
      { wrapper: Wrapper },
    );
    expect(result.current.message).toEqual(defaultMessage);

    act(() => {
      result.current.setMessage(updatedMessage);
    });

    expect(result.current.message).toEqual(updatedMessage);
  });
});

As you can see we are creating a Wrapper component that contains the provider which we are then passing into the renderHook method.

The wrapper component accepts children as props so that it can render any components passed into it which is needed for the wrapper to work with renderHook.

If you have a project with many components reliant upon the provider, like in redux based application, for example, then it would also be beneficial to extract the wrapper component and then share it across all your tests so you don’t have to re-create it every time.

How to test state updates in a custom react hook

Now we know how to test with providers by using a wrapper, let’s take a look at how we can test state updates using the useState hook.

To be able to test useState in our custom React hook we are going to need a few things.

Firstly, we will need a custom react hook that contains a state, and if we are testing it the hook should already be exposing a way to update the state somehow.

When I say “already be exposing”, I mean that you should not change the way your hook works for a test because your test should be testing what is there.

Your custom hooks state at this point will be updated either implicitly or explicitly.

All we need to do from here is to cause the state to update.

Let’s look at an example:

import { useState, useEffect } from 'react';
import translate from './translate';
import messages from './messages';

export default function useLocalisedIdMessage() {
  const [id, setId] = useState(null);
  const [message, setMessage] = useState(null);

  useEffect(() => {
    if (id) {
      setMessage(translate(messages.idMessage(id)));
    }
  }, [id, setMessage]);

  return {
    message,
    setId,
    id,
  };
}

It goes without saying that there are better ways to do this, but this is just an example so we can demonstrate how you would test a state change for a custom hook.

In this example setting the state is explicit and obvious, so all we need to do is call the setId function that is returned from the state.

To call this function we need to do it from within an act function because we will be triggering a state update and causing re-renders to happen.

Here is how this would look in our unit test for this custom hook:

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

describe('useLocalisedIdMessage', () => {
  it('Should update the message', () => {
    const id = 'test-id';
    const { result } = renderHook(useLocalisedIdMessage);
    expect(result.current.id).toBeNull();

    act(() => {
      result.current.setId(id);
    });

    expect(result.current.id).toEqual(id);
  });
});
Passing test using custom react hook with state

And there we have how to test useState in a custom react hook.

How to test effects in a custom react hook

Much like testing the state, we might also need to consider how to test a useEffect hook in our custom hook.

This is going to be very similar to testing with useState because we are looking to test a side effect of a state change.

A side effect could be anything, it could be calling a callback, firing a network request, modifying a value and more.

In this example we will look at modifying a value.

The useEffect in this example is using the id to provide the id to the user in a localised format.

So for our test we just need to make sure that the localised format has changed as we expect it should have after the state change.

Let’s add the following to our test:

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

describe('useLocalisedIdMessage', () => {
  it('Should update the message', () => {
    const id = 'test-id';
    const { result } = renderHook(useLocalisedIdMessage);
    expect(result.current.id).toBeNull();

    act(() => {
      result.current.setId(id);
    });

    expect(result.current.message).toEqual(`Your user ID is: ${id}`);
  });
});
Passing test using custom react hook with effects

As you can see we are asserting that the user message now has the correct localised message as well as the correct id.

We now have a working test suite of testing a state update via useState and a lifecycle effect via useEffect.

How to test custom react hooks with jest (Without react-hooks testing-library)

So, now we know one of the easiest ways to test custom react hooks, are there other ways to do it?

And what if you don’t want to install a new library into your codebase?

Well, don’t worry, it is still possible to test custom react hooks in jest without the react-hooks testing-library and in this section we will look at doing just that and creating unit tests in jest without using the library.

So first things first, we need to have a custom hook to be able to test, so let’s re-use the simple example custom react hook we created before.

Next we need to do is to create a test suite for this hook.

Depending on how you have jest setup this could be different, but for most cases, we only need to create a simple file that will be named ExampleCustomReactHook.spec.jsx.

You can replace the *.spec.jsx with *.test.jsx or even just nest it into a folder named __tests__, or a combination of all three if you wanted.

Now that we have created a file let’s add some basic setup to it like in the following example:

import { render } from '@testing-library/react';

describe('useLocalisedIdMessage', () => {
  it('Should update user message', () => {
  });
});

Here you can use whichever testing library you like such as enzyme or the react testing-library (or you can even test using the only React’s renderDOM and jest alone!) but for this post I am going to be using the react testing-library for our example.

The only thing from the testing library that we need will be the render method to be able to make assertions against the DOM and possibly the act function for effects.

Now we have our setup complete we need to start looking into how we can render and assert our custom hook.

To do this we need to create a wrapper component to house our hook that will let us be able to test it against DOM changes.

import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import useLocalisedIdMessage from './useLocalisedIdMessage';


const UserIdMessageExample = ({ testId }) => {
  const { message, id, setId } = useLocalisedIdMessage();

  return (
    <p>{message || ''}</p>
    <button onClick={() => setId(testId)}>Set ID</button>
  )
}

describe('useLocalisedIdMessage', () => {
  it('Should update user message', () => {
  });
});

All that is left to do now for our custom hook test is to assert some changes!

To do this we will make use of the render method to make a few simple assertions about how the data is being affected in the DOM.

Let’s see how that will look in this unit test example:

import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import useLocalisedIdMessage from './useLocalisedIdMessage';


const UserIdMessageExample = ({ testId }) => {
  const { message, id, setId } = useLocalisedIdMessage();

  return (
    <p>{message || ''}</p>
    <button onClick={() => setId(testId)}>Set ID</button>
  )
}

describe('useLocalisedIdMessage', () => {
  it('Should update user message', () => {
    const testId = 'test-id';
    const { getByText, queryByText } = render(
      <UserIdMessageExample testId={testId} />
    );

    expect(queryByText(`Your user ID is: ${id}`)).not.toBeInTheDocument();
    userEvent.click(getByText('Set ID'));
    expect(getByText(`Your user ID is: ${id}`)).toBeInTheDocument();
  });
});

All that is happening here is that we have created our custom hook, and we are testing it by making use of the hook in a react component which is the place it will be used and making DOM changes to reflect what it is doing.

One of the best ways of testing code is to do it from the perspective of how it affects the user.

In this scenario, we are doing just that.

We are using the hook in the DOM and testing to see how it affects the user.

From here, there are a few more things we can do to improve our tests for the custom react hook.

These next steps might not always be applicable but are useful to know if you have any hard to test bits of code.

The main thing to do now is to separate some of the functions and functionality from the custom react hook.

Doing this means we can test each bit of functionality separately, which makes testing the entire hook much easier.

How to test custom react hooks with TypeScript

Now that we have covered some ways in which you can test custom react hooks in JavaScript, it is time to look at how we can do the same with TypeScript.

Because TypeScript is based on/built from JavaScript it will be pretty easy to add in TypeScript to our tests and hooks.

I won’t go over how to integrate TypeScript into your project in this post because it is highly dependent on each project, but if you are using create-react-app you can follow this guide on how to add in TypeScript to your create-react-app project.

Now let’s dive into how we can update and test custom react hooks with TypeScript.

Firstly, we need to change the file names we have used from JavaScript/JSX to TypeScript/TSX.

So now our component file will be ExampleCustomReactHook.tsx and our test file will be ExampleCustomReactHook.spec.tsx.

Next, we need to create an interface for the return type of our custom react hook.

An interface is just a way of describing to TypeScript what our object structure will look like.

All we need to do in the interface, and inTypeScript is say what types are being returned. For example, if you are returning a string, we need to just add in some TypeScript to say we are returning a string, or the same for a number and so on.

The interface is just a way of doing this for an object which has properties that you want to describe.

With that in mind for our hook we will create the following interfaces:

export interface LocalisedIdMessageProps {}

export interface LocalisedIdMessage {
  setId: (id: string) => void
  message?: string | null
  id?: string | null
}

As you can see we are describing what the hook returns and if those fields can be undefined or not and so on.

You may notice here that we are exporting the interfaces, this is important so we are able to re-use it in our test like so:

import useLocalisedIdMessage, {
  LocalisedIdMessageProps,
  LocalisedIdMessage,
} from './useLocalisedIdMessage';

Now all we need to do is attach it to the renderHook method so that the TypeScript compiler can inform us about which properties will come back from the renderHook method.

We can do this by adding in the interfaces like so:

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

describe('useLocalisedIdMessage', () => {
  it('Should update the message', () => {
    const id = 'test-id';
    const { result } = renderHook<LocalisedIdMessageProps, LocalisedIdMessage>(useLocalisedIdMessage);
    expect(result.current.id).toBeNull();

    act(() => {
      result.current.setId(id);
    });

    expect(result.current.message).toEqual(`Your user ID is: ${id}`);
  });
});

And now you can see we have a working custom react hook in TypeScript along with our TypeScript unit tests with jest.

How to test custom react hooks that use graphql

We know how to pass providers into the renderHook method for our tests, so now it is time we can start to add in more complex tests for our custom react hook.

A common use for custom react hooks is to make various network requests, and for the most part this will involve mocking the response in one way or another from these requests so you can test against the data you expect.

An example of this kind of hooks would be using apollo client with useQuery to make requests to a graphql server.

If you want to read more about useQuery or apollo client make sure you checkout the official apollo docs as well as my article about how to use multiple useQuery hooks in sequence.

Let’s briefly go over how we can test custom react hooks that uses apollo client with graphql.

Firstly we need to create a basic apollo provider component, we can do this by adding the following to the root of our app:

import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
} from '@apollo/client';
import App from './App';

const client = new ApolloClient({
  uri: 'https://https://www.apollographql.com/docs/react/get-started',
  cache: new InMemoryCache()
});

function AppRoot() {
  return (
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>,
  );
}

render(
  <AppRoot />,
  document.getElementById('root'),
);

Next let’s create a basic custom react hook that uses useQuery to request data from an apollo graphql server.

import { useState, useEffect } from 'react';
import {
  useQuery,
  gql,
} from '@apollo/client';

export const GetPlanetsQuery = gql`
  query GetPlanets {
    planets {
      name
    }
  }
`;

export default function usePlanetsCustomHook() {
  const defaultPlanet = 'Mars';
  const { loading, error, data } = useQuery(GetPlanetsQuery);

  return {
    defaultPlanet,
    planets: data?.planets,
    planetsAsList: data?.planets?.map(planet => planet.name)?.join(', '),
  };
}

Now we can take a look at how we can test this in our jest unit tests.

Here we are going to make use of a provider that comes from apollo graphql called Mocked Provider.

Just like in a previous section we now need to add this provider into our renderHook as a wrapper.

import { renderHook, act } from '@testing-library/react-hooks';
import { MockedProvider } from '@apollo/client/testing';
import usePlanetsCustomHook, { GetPlanetsQuery } from './usePlanetsCustomHook';

const mocks = [];

const Wrapper = ({ children }) => (
  <MockedProvider mocks={mocks} addTypename={false}>
    {children}
  </MockedProvider>
);

describe('usePlanetsCustomHook', () => {
  it('Should update the message', async () => {
    const id = 'test-id';
    const { result } = renderHook(
      usePlanetsCustomHook,
      { wrapper: Wrapper },
    );

    expect(result.current.defaultPlanet).toEqual('Mars');

    // this is needed for MockedProvider and the useQuery we are using
    // see https://www.apollographql.com/docs/react/development-testing/testing/#testing-the-success-state
    // to find out more
    await new Promise(resolve => setTimeout(resolve, 0));

    expect(result.current.planets).toEqual('Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune');
  });
});

Now the only things left for us to add in are the mock requests, and the actual unit tests.

It is important to remember that when you test using MockedProvider you must provide and return exactly what is expected.

This means you have to provide the variables and the correct data in the response otherwise the tests will fail.

So let’s now add this in for our custom hook example:

import { renderHook, act } from '@testing-library/react-hooks';
import { MockedProvider } from '@apollo/client/testing';
import usePlanetsCustomHook, { GetPlanetsQuery } from './usePlanetsCustomHook';

const mocks = [{
    request: {
      query: GetPlanetsQuery,
    },
    result: {
      data: {
        planets: [
          {
            name: 'Mercury',
          },
          {
            name: 'Venus',
          }, 
          {
            name: 'Earth',
          }, 
          {
            name: 'Mars',
          }, 
          {
            name: 'Jupiter',
          }, 
          {
            name: 'Saturn',
          }, 
          {
            name: 'Uranus',
          }, 
          {
            name: 'Neptune',
          },
        ],
      },
    },
  }];

const Wrapper = ({ children }) => (
  <MockedProvider mocks={mocks} addTypename={false}>
    {children}
  </MockedProvider>
);

describe('usePlanetsCustomHook', () => {
  it('Should update the message', async () => {
    const id = 'test-id';
    const { result } = renderHook(
      usePlanetsCustomHook,
      { wrapper: Wrapper },
    );

    expect(result.current.defaultPlanet).toEqual('Mars');

    // this is needed for MockedProvider and the useQuery we are using
    await new Promise(resolve => setTimeout(resolve, 0));

    expect(result.current.planetsAsList).toEqual(
      'Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune'
    );
  });
});

How to test custom react hooks that use fetch

The last thing we are going to cover for how to test custom react hooks, is about how we can test a custom hook that is using fetch.

Like I mentioned in the last section, network requests will need to be mocked one way or another.

There are many options for this, and we have covered just about everything you need to be able to implement this on your own.

However, we will briefly look at how we can test fetch in our hook by using msw (Mock Service Worker).

So what is msw?

Msw is a service worker that can intercept and then mock network requests for your app, service workers only work in browsers but this library can also support node and testing .

This means you can test and develop your app in the same way without having to worry about mocking any network requests like we usually do for testing.

There are many benefits to it but we will be focusing on what this means for testing our custom react hook using fetch.

Because msw does the mocking for us,we can let our custom hook make network requests as it would normally even though it is a test.

All we need to do is get msw set up, tell it what data we want to return when we make a given request and let jest and msw handle the rest.

I’m not going to cover how to integrate msw to your app in this article because it deserves it’s own, however you can follow the official installation guide and setup guide here.

Once you have that installed you will be able to test your hooks and network calls as if you were connected to the actual server.

Summary

That covers everything you need to know to get up and running with testing a custom react hook.

Be sure to check out some of my other posts for more information just like this about React, TypeScript and JavaScript.

I hope this post has helped, but before you go I highly recommend that you checkout Pluralsight if you are interested in improving your software engineering skills, such as React, JavaScript, or Typescript (and much more).

Not only does Pluralsight provide you with all the learning material you need but it also let's you learn in a fun, interactive way and gives you the ability to see your current progress and level in a certain skill via their Skill Assessment tool which can help you see what you need to learn next to keep improving!

Learn React, JavaScript and TypeScript

Learn React, JavaScript and TypeScript

Join the platform that top tier companies are using.
Master the skills you need to succeed as a software engineer and take your career to the next level with Pluralsight.

Start here

Some graphics used on this post were made using icons from flaticon.

Latest Posts

Learn React, JavaScript and TypeScript

Learn React, JavaScript and TypeScript

Join the platform that top tier companies are using.
Master the skills you need to succeed as a software engineer and take your career to the next level with Pluralsight.

Start here

Earn more and take your coding career to the next level.

Here you will find my personal recomendations to you, for full disclosure I earn a small commission from some of these links, but I only recommend what I trust and personally use.