Jest: How to mock a single hook from a module

Jest: How to mock a single hook from a module

Crysfel Villa Programming

Today I needed to write a test for a component that uses a hook from our common library, but at the same time I needed to use other components in the test.

Initially I mocked the whole module and got all previous tests to fail, the issue was that I mocked everything and no other component was working as expected.

Here's the code I wanted to test.

import { Text, File, useToast } from '@my-library/ui';
import axios from 'axios';

export default function Uploader({ userId ) {
  const showToast = useToast();
  const onChange = async (event) => {
    const file = event.target.files[0];
    const form = new FormData();

    form.append('file', file);
    form.append('user_id', userId);

    try {
      await axios.post('/api/users/profile', form);

      showToast({ message: 'Successfully uploaded!' });
    } catch (error) {
      showToast({ message: 'There was an error' });
    }
  };

  return (
    <div>
      <Text>Please select a file to Upload</Text>
      <File onChange={onChange} />
    </div>
  );
}

I simplified the code, but basically is a component that allows users to select a file and upload it to the server, for example a profile picture or whatever.

In order to test this code we need to mock a file, fire the event on the input element, then check if the axios.post method was called as well as showToast with the right message.

Let's start mocking axios, we can mock the whole thing for this one.

import axios from 'axios';

jest.mock('axios');

While we are here, we should also mock the hook from our library, but we need to keep the original implementation for all other components.

import { useToast } from '@my-library/ui'; // Step 1
import axios from 'axios';

jest.mock('axios');
jest.mock('@my-library/ui', () => { // Step 2
  const original = jest.requireActual('@my-library/ui');
  return {
    ...original,
    useToast: jest.fn(),
  };
});

First we need to import the hook into our test, we are going to use it in the expectation to check if it was called or not.

The second step mocks the whole library, but in the second parameter we can use a function to manually mock what we really need. The first thing we do here is to the get actual implementation and return that for all components, finally we overwrite the hook only with a mock function.

We need to return a mock function so we can set return values when preparing our test.

With that in place we can proceed to write our test.

describe('Uploader', () => {
  beforeEach(() => {
    jest.resetAllMocks(); // Step 1
  });

  it('uploads a new logo', async () => {
    jest. // Step 2
      .spyOn(axios, 'post')
      .mockImplementation()
      .mockResolvedValue({
        success: true,
      });

    // Step 3
    const { container } = render(<Uploader userId={1}  />);

  });
});

The first thing we do is to reset the mocks before each test, this is necessary to make sure we start each test with a clean state.

In the second step we need to prepare the mock for axios, we can use spyOn and mock the returned value.

Finally we render the component sending the userId prop, this prop will be used in the body form.

Once we have our test ready we proceed with the second part of an effective test, that's executing some actions on the component. In this case we are going to mimic the user selection a file from their file system.

// Step 1
const blob = new Blob(['test']);
const file = new File([blob], 'img.png', {
  type: 'image/png',
});

// Step 2
fireEvent.change(container.querySelector('input'), {
  target: { files: [file] } 
});

First we mock a new file using Blob, we use this object in the second step when firing the event.

The second steps fires the change event on the input element, we set an array of files inside a target object, we are basically mocking the browser behavior.

That's all, we need to move on to the next part of our test, which is making sure the file gets uploaded to the server.

expect(axios.post).toHaveBeenCalledTimes(1);

This one is very straight forward, we just need to make sure the axios post method gets executed.

So now let's test the toast. But for this one we are going to test the error message, let's start by setting up our test with all the mocks we need.

it('shows error when server fails', async () => {  
  // Step 1
  axios.post.mockImplementation(() => throw new Error())

  // Step 2
  const mockedToast = jest.fn();
  useToast.mockImplementation(() => mockedToast);

  // Step 3
  const { container } = render(<Uploader userId={1}  />);
});

First we mock the post method from axios, we are going to throw an error to mock a failure response from the server.

In the second step we are going to mock the hook, this hook returns another function, the one that actually shows the toast message, but we don't really care about showing the message in the test, we just want to make sure this function got executed, hence we return mockedToast.

Finally we render the component with to test.

Next we need to act on the component, we are going to mimic the user selection and fire the event from the input, exactly the same as we did in our previous test, so I don't talk about it much here.

const blob = new Blob(['test']);
const file = new File([blob], 'img.svg', {
  type: 'image/svg+xml',
});

fireEvent.change(container.querySelector('input'), {
  target: { files: [file] }
});

Finally we need to make sure the mock got called as follow.

expect(mockedToast).toHaveBeenCalledTimes(1);
expect(mockedToast).toHaveBeenCalledWith({
  message: 'There was an error',
});

That's it! The final test looks like this:

import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { useToast } from '@my-library/ui';
import axios from 'axios';
import Uploader from './Uploader';

jest.mock('axios');
jest.mock('@my-library/ui', () => {
  const original = jest.requireActual('@my-library/ui');
  return {
    ...original,
    useToast: jest.fn(),
  };
});


describe('Uploader', () => {
  beforeEach(() => {
    jest.resetAllMocks();
  });

it('uploads a new logo', async () => {
    jest
      .spyOn(axios, 'post')
      .mockImplementation()
      .mockResolvedValue({
        success: true,
      });

    const { container } = render(<Uploader userId={1}  />);

    const blob = new Blob(['test']);
    const file = new File([blob], 'img.png', {
      type: 'image/png',
    });

    fireEvent.change(container.querySelector('input'), { target: { files: [file] } });

    expect(axios.post).toHaveBeenCalledTimes(1);
  });

  it('shows error when server fails', async () => {  
    axios.post.mockImplementation(() => throw new Error())
    const mockedToast = jest.fn();
    useToast.mockImplementation(() => mockedToast);

     const { container } = render(<Uploader userId={1}  />);

      const blob = new Blob(['test']);
      const file = new File([blob], 'img.svg', {
        type: 'image/svg+xml',
      });

      fireEvent.change(container.querySelector('input'), {
        target: { files: [file] } 
      });

      expect(mockedToast).toHaveBeenCalledWith({
        message: 'There was an error',
      });
  });
});

Happy coding folks!

Did you like this post?

If you enjoyed this post or learned something new, make sure to subscribe to our newsletter! We will let you know when a new post gets published!

Article by Crysfel Villa

I'm a Sr Software Engineer who enjoys crafting software, I've been working remotely for the last 10 years, leading projects, writing books, training teams, and mentoring new devs. I'm the tech lead of @codigcoach