circuit

Mock Your Hooks to Make Testing Simpler

Mocking with Jest can have a weird learning curve. Let's see how to mock those hooks.




Photo by Steve Johnson: https://www.pexels.com/photo/selective-focus-of-stainless-steel-hook-1256916/

In this article, we'll be interested in mocking our hooks to make the testing of our react components easier.

The goal of unit testing in React is to be able to test everything within a specific file without having any interference from another module.

Mocking allows us to keep a constant result from other modules by ignoring their logic and forcing them to return a specific value.

So let's get started!

Getting ready

For the sake of this article, I'll assume that you have a React environment set up with Jest and React Testing library. If not, there are plenty of tutorials online to set this up in a snap!

Before starting our tutorial, we'll prepare some files.

First, we'll need a hook to mock:

// useNumber.js
const useNumber = () => {
    return 4;
}
export default useNumber;

We want to keep it very simple here, just a simple “hook” called useNumber and returning the number 4. We don't need any more complexity here!

Then we'll need a React component that is using this hook:

// App.js
import useNumber from './useNumber';

function App() {
  const number = useNumber();
  return (
    <div>
      {number}
    </div>
  );
}

Here once again, very simple component, we don't need anything more!

Finally, we are writing our test. As we are mocking the hook, our final goal is our hook not to return 4:

// App.test.js
import {render} from '@testing-library/react';
import App from './App';

describe('App', () => {
    it('The hook should be mocked', () => {
        const {queryByText} = render(<App />);
        expect(queryByText('3')).not.toBeNull();
        expect(queryByText('4')).toBeNull();
    })
})

Now we are ready to get started!

Method 1: Mocking at the top level

In this article, we are going to be mocking our hook in two different ways. (I'll talk about which one to pick at the end of the article. Stay tuned!)

The first method is mocking our hook at the top level of our test. Doing so will make this mocking enabled for all tests within this file.

***jest***.mock('./useNumber', () => {
    return () => 3;
});

describe('App', () => {
    it('The hook should be mocked', () => {
        const {queryByText} = render(<App />);
        expect(queryByText('3')).not.toBeNull();
        expect(queryByText('4')).toBeNull();
    })
})

We are using jest.mock to do the mocking. As a first parameter, it's taking the path to the file to mock.

Keep in mind that if your test file is not at the same level as the file to be mocked, the path will differ. The relative path is always relative to the test file and not the tested component.

The second parameter taken by jest.mock is the implementation of the new module (the mocked module). It's always structured like this:

() => <implementation>

The first arrow function is called a factory and needs to return the new implementation. Doing the following is not valid:

***jest***.mock('./useNumber', {
    return () => 3;
});

Now to come back to our new implementation, we are returning a new function (a hook is a function!) that is returning 3.

And here we go! Our hook is now mocked! Give it a go, your test should (will) pass!

Method 2: Mocking per test

There is another way to mock your hook which is at the test level. This kind of mocking is going to be per test and allows you to have different return values depending on your tests.

First of all, we need to do a small change in our jest.mock call:

***jest***.mock('./useNumber');

As you can see, I removed the second parameter as we are going to directly mock the implementation in our tests.

Note that removing the default value won't set the original hook as default. Instead, the hook will be automatically mocked and won't return any value. I'll explain more at the end of the article.

Now, we need to mock the return value of the hook directly into our test:

import useNumber from './useNumber'
...
it('The hook should be mocked', () => {
    useNumber.mockReturnValue(3);
    const {queryByText} = render(<App />);
    expect(queryByText('3')).not.toBeNull();
    expect(queryByText('4')).toBeNull();
})

And here we go! Now, we mocked the return value of the hook and for this specific test, it's going to return 3.

It's also possible to use mockImplementation instead of mockReturnValue if you prefer:

useNumber.mockImplementation(() => 3);

Note how I had to import useNumber in our file. It is because I need to act on the mock that jest.mock does to specify a proper return value.

Which method is better?

The simple answer is none.

It's not a surprise that in software programming,** there is rarely one answer to a problem**. It'll usually depend on what you need.

First method: a static, global approach

The first method is the best if you just wish to stick your hook to a specific value and forget about it for the rest of your tests. It'll stay static, at the top of your file and will perfectly do that job.

Second method: More flexible but with a price

The second method is usually the best if you wanna play with the hook's return value to trigger different behaviors in your components. For the case of a boolean, you may want to have the hook returning both false and true depending on the test to be sure that your component adapts to it.

However, there is a small downside to this method: You need to be sure that you define the return value for **ALL **tests. In the case where you don't, jest will still return jest.fn but it may create some false positive in your results. So be careful!

I hope that this small article helped you to understand this better! Mocking can be a bit tricky sometimes as Jest might throw some futuristic errors at you if you forget an arrow function somewhere. But where you get it, it's actually quite simple!

I hope you liked this article. If you did, don't hesitate to follow me or leave some claps as they help a lot!

Have a wonderful day (or night depending on the timezone)!




Continue Learning