DEV Community

Patrick Geiger
Patrick Geiger

Posted on

React and TypeScript Testing: Mocking Functions with Jest

Hello, in this article we're going to talk about mocking functions with Jest and TypeScript in React. Specifically, we're going to talk about how to mock a function that you pass into a component using Jest.

Now mocking functions with Jest, in general, is pretty straightforward.

const mockFunction = jest.fn();

That's all you really need to get started using a mock function that you can then monitor. For example, if pushing a button should call a function your assertion, after clicking the button, can be the following:

expect(mockFunction).toHaveBeenCalledTimes(1);

This is, of course, for functions that are being passed into your component. So, if using Enzyme, your code may look like this:

const mockFunction = jest.fn();
const wrapper = mount(<MyComponent onChange={mockFunction} />);

This works fine for the most part. But this does type things with any, plus what do you do if you need create a variable but don't want to initialize it immediately? You have to type it of course, but what typing do you use? Consider the following:

let mockFunction: jest.Mock<any, any>;

This would work fine in the following case:

let mockFunction: jest.Mock<any, any>;
let wrapper: ReactWrapper;

beforeEach(() => {
  wrapper = mount(<MyComponent onChange={mockFunction} />);
});

In some cases, this is about as far as you need to go. After all, the function itself is fake anyway. Typing it further may not be necessary depending on your use case. But it'd be good form to make sure it's properly typed and you're not using any. You may even need it to be typed because it provides useful information, or you get a linting error because of it. So how would we go about doing that?

It's actually relatively straightforward. I'll break it down:

Looking at jest.mock<any, any>, the jest.mock part stays. As for the <any, any> it's helpful to look at it as <return, input>. The first value is what you plan on returning, while the second value is actually an array of the inputs. So what if we take in a string and return nothing?

let mockFunction: jest.Mock<void, [ string ]>;

It's simple once you know what goes where. Let's see it in action when it comes to assignment:

let mockFunction: jest.Mock<boolean, [string]>;

mockFunction = jest.fn((myString: string) => {
  return true;
});

In the above case we threw in a return value. Sometimes it's necessary to mock a return when you're testing. After all, you don't really care how the function got the return. These unit tests are for this component in particular, and whatever function is being passed in, in the actual code, should have its own unit tests. So you just need your return so you can move on.

Let's take a look at a more complicated example, this time with promises.

Consider that you have a voting component. The component itself consists of two buttons that allow the user to like or dislike something. You need to persist this on the back end as well. One solution to this is to pass a function into the voting component that talks to the back end in some way. There may be better solutions, but for the sake of this example we're going to go with this one.

The exact implementation isn't important. Let's just say the function that talks to your back end takes in an string id and a boolean value, and returns a Promise and this function is passed in through an onChange prop on the component. That onChange prop is then called when one of the buttons are clicked.

interface IResponse {
  status: "SUCCESS" | "ERROR";
}

let mockFunction: jest.Mock<Promise<IResponse>, [string, boolean]>;

let wrapper: ReactWrapper;

beforeEach(() => {
  mockFunction = jest.fn((id: string, vote: boolean) =>
    Promise.resolve({ status: "SUCCESS" })
  );

  wrapper = mount(<Votes onChange={mockFunction} />);
});

Of course your IResponse should probably be in its own typescript file. Regardless, this is the basic structure you would use for something like this. If you wanted to test your error handling when receiving an error from the backend, you can just switch the status over to "ERROR". And if you wanted to test the promise failing entirely, you can use reject instead of resolve.

Hopefully this helped somebody out there.

Top comments (1)

Collapse
 
oleivahn profile image
Omar Leiva

Way to blow my brain up this early in the morning. Good clear and concise read though! 🤙🏼