DEV Community

Jennifer Lyn Parsons
Jennifer Lyn Parsons

Posted on • Updated on

A bit about Jest mocks

I've been learning Jest and applying it to a side project built in React. As I run across various challenges I've cobbled together some things that I thought might be helpful for others new to testing. A few bits of information here were a challenge to locate. I came by them via my network and some asking around and trial and error. This post is my way of paying that forward.

My plan is to give a very brief overview of mocks in general, a few examples from my real life codebase, and provide a few resources for further information.

Mocks in general

From the Jest docs:

"Mock functions make it easy to test the links between code by erasing the actual implementation of a function, capturing calls to the function (and the parameters passed in those calls), capturing instances of constructor functions when instantiated with new, and allowing test-time configuration of return values."

That's a mouthful. What it basically means is that a mock replaces a function call within the test with a fake version. That means you grab the name of a function and replace it with a different implementation.

Does this mean you're just rewriting that function inside the test? I can see where you might think that. However, with this mock version of the function, you only need the bare minimum of code. It can return a basic value and should not include any logic. You aren't testing how this function works after all, instead you're testing the function that calls this function.

Mocks in Jest

Jest has a couple of built-in methods for creating mock functions. The most basic is jest.fn(), but I haven't found that useful on its own so far. In my codebase, which is a MERN stack project using Redux, I've found that method most useful inside other mocks. Your mileage may vary, as they say.

I've used a couple of Jest mocks in my own tests. I'm going to give the examples here and walk through them as best I can.

The jest.mock() function call takes two arguments. The first is a string that contains the path to the module that contains the function being called (or the name of the Node module). The second is an optional function that is used in place of the original function.

Mocks in Jest also have a special set of assertions. Since they are not intended to provide the same functionality as the original function calls, mocks are generally used only to assert that a function has been called. We can determine how many times it's been called or what arguments are used to call it, but that's about it.

Examples

The code for the tests and the code being tested (in stripped down versions) can all be found in this gist.

Mocking a module function

This is a mock I have set up to provide a faked function call. The pathname is given and then the result of that call is faked with () => jest.fn(), a function that returns a function. This reflects the structure of the setAuthToken function and for the test, that's all we need.

jest.mock("../../utils/setAuthToken", () => jest.fn());

That mock is used in the test below soley to ensure that inside of my loginAction() action creator, the setAuthToken function has been called. Elsewhere, I've tested that setAuthToken returns the expected response, but this is a unit test, so I'm only looking to ensure the call is made.

test("it sets the JWT token to the response from the POST", async () => {
  await store.dispatch(loginAction(mockLoginData));
  expect(setAuthToken).toHaveBeenCalledWith(mockResponse.token);
});

Mocking a Node module

This mock sets up the jwt-decode node module fake so that when it is used in the next test, I can be sure that I once again am getting a call out to the correct function.

jest.mock("jwt-decode");

Here is the test where that is used. Note that I'm only looking to ensure that .toHaveBeenCalledWith is a true assertion.

test("it decodes the token with jwt_decode", async () => {
  await store.dispatch(loginAction(mockLoginData));
  expect(jwt_decode).toHaveBeenCalledWith(mockResponse.token);
});

Mocking an API call

This mock is being used to fake an API call, in this case, a post that returns a Promise that is resolved with some fake data. Note the function inside it does a bit more than the first example. Here I'm not just returning an anoymous function wrapped around jest.fn(). Instead, here I'm returning an object which currently has a post key and the jest.fn() function which contains an anonymous function that returns a Promise. Whew!

Additionally, note that the object it returns can be filled out with the rest of the API methods in comma separated multiple key/value pairs.

jest.mock("../../lib/api", () => ({
  post: jest.fn(() => Promise.resolve(mockResponse))
}));

Here's what the test that uses that mock looks like. I've faked the API.post property in the mock. The original version of that function takes two arguments, the path for the call and the data passed to the backend. Here I'm checking that, once again, the function was called with the correct data. In this case, I am also checking that returns a resolved Promise.

test("it calls 'post' on the API with the correct path and the user data", () => {
  store.dispatch(loginAction(mockLoginData));
  expect(API.post).toHaveBeenCalledWith("/users/login", mockLoginData);
});

Mocking with a default implementation

This mock combines a few of the mocks above, with an added twist! Here I'm mocking the react-router-dom node module, just like with the jwt-decode module. However, I do not want to mock out the entire module. I need to keep Route intact and only mock Redirect. To accomplish this, I am using a method similar to the API.post mock above and returning an object. I'm using the requireActual() function to grab all of the original, real methods in the module. Inside the return, I'm first using the spread operator to add those original methods. I'm then overwriting only the Redirect method.

Did you see the little new twist on Redirect? It's the .mockImplementation() call. Because unlike the first example where I only needed a function and unlike the API call where I only needed a resolved Promise, in this case I need some kind of returned value. It doesn't have to be a value that matches what would be return in the original code, but I do need to return something. The intent of .mockImplementation() is to create an actual, if fake, implementation.

jest.mock("react-router-dom", () => {
  const original = jest.requireActual("react-router-dom");
  return {
    ...original,
    Redirect: jest.fn().mockImplementation(() => null)
  };
});

Here is the test where that mock is used. In this case I'm making sure that when the user is logged out that they are redirected somewhere else. Because I'm not trying to test react-router-dom itself, it's enough to know that a redirect has happened without worrying about where to.

test("PrivateRoute should redicted to the login page if user is logged out", () => {
  const store = makeMockStore(loggedOutState);
  let wrapper = mount(
    <MemoryRouter initialEntries={["/dashboard"]} initialIndex={0}>
      <PrivateRoute
        path="/dashboard"
        component={GenericComponent}
        store={store}
      />
    </MemoryRouter>
  );
  expect(wrapper.find(Redirect).exists()).toBe(true);
});

Spies!

Spies work a little differently, but are still a type of mock. From the official docs: "Creates a mock function similar to jest.fn but also tracks calls to object[methodName]. Returns a Jest mock function." What this means is that the function being tracked must exist as a method name on an object. For example, in this case, I'm exporting my methods from authActions.js like this:

const authActions = { loginAction, registerUser, logoutUser, setCurrentUser };

export default authActions;

I did this in order to track that the setCurrentUser method was being called inside the loginAction method. The reasons this was necessary are beyond the scope of this article, but there's a really great explanation here. The tl;dr is that it has to do with the way Babel compiles JavaScript.

.spyOn also calls the spied method and does not allow you to overwrite the original implementation.

There is no separate mock for this one, I'm using .spyOn directly in the test. The function takes two arguments, the object where the function exists and the name of the function. I can then check that when loginAction, which also lives inside the authActions object, is called, it calls setCurrentUser one time.

test("it sets the current user action", async () => {
  let currentUserSpy = jest.spyOn(authActions, "setCurrentUser");
  await store.dispatch(authActions.loginAction(mockLoginData));
  expect(currentUserSpy).toHaveBeenCalledTimes(1);
});

Summary

Jest mocks, and mocking in general, can be a tricky but they're useful in unit testing because they allow you to test the code you've written without worrying about dependencies. I hope a few of these details clarify some things for you and that by sharing this information someone else might have a little bit easier time learning to write tests.

Please let me know if you find any errors, I'm looking to learn more!

Resources

I also want to thank the gang over at WeAllJS for their invaluable help as I figured this all out.

Top comments (0)