Blog

Ideas and insights from our team

Testing your React components (part 2)


Disclaimer: due to the amount of information, this article is divided into three parts. The first part consists of a small intro to React testing, which includes configuration and snapshot/state changes tests. The second part shows how to test components using mocks and the third part brings more complex test cases with components that are using redux.

Welcome back! In my last article, we talked about the basics of React testing with Jest and Enzyme - but that was just a small example of the things you can do with tests. In this article, I'll dive a little deeper and present to you the concepts of mocks and how Jest makes it pretty easy to work with them. If you're interested in the source code for this article (including all of its parts), you can get it here.

Packages and configuration

Since Jest comes with complete mock support, you won't need to install anything more than what has already been mentioned in the previous article, which you can read here (please note that we'll still be using Enzyme to render components). While I'll give you as much information as you'll need to get started and understand mocks, reading Jest's documentation on those is highly recommended, as they documented it pretty well. Here's a list of most of the mock-related pages from Jest's own documentation:

Reading these pages after finishing this article will help you understand a lot more about the subject.

What is a "mock"?

It can be very confusing to talk about mocks without understanding what they are and what they can do, so let's take this section to understand a little more about them; if you already know what I'm talking about, feel free to jump to the next section.

Mocks are nothing out of the ordinary and it's not an exclusive functionality of Jest or JavaScript – it's also present in test suites of many other programming languages. Basically, they replace a module/function/class/object with its mocked version. So, when I say that a function is a mock (or that it was mocked), it means that it's been replaced with a fake function that doesn't actually run anything and does not relate to its original code any longer. That would be applicable to anything you choose to mock. This doesn't sound like it's a useful feature, right? Well, when you mock something, you'll also have the power to spy on everything that it was asked to do (or everything that was done to it, such as how many times it was called and what arguments were passed in each call). You'll also be able to change and control what it returns or when it'll return something – or even replace its whole implementation.

When you should use mocks

There are several cases in which you can choose to use mocks. The most common cases though, are the ones when:

  • You want to isolate a specific function that have too many dependencies to determine its output (as in when they call other functions inside them). Now, when you're doing unit tests, there are times when you don't care about the results of other functions, except the one that you're actually testing. And that's where the mocks come in: if you mock them, you won't have to care about their results anymore, as they can be whatever you want; so an API call can always return what you tell it to return without actually asking the server, for example, and you won't have to wait for some other function that takes a very long time to run when you're not even testing it directly.

  • You want to keep count and be sure of how many times a function has been called and what were the arguments passed to it. Sometimes it's very important to know that the form is only calling onSubmit once and only once, or that you're passing the right arguments to do so.

Jest mocks

As mentioned before, Jest comes with mock support by default. So if you want to mock something, there are four ways to do so:

  • By mocking a function const mockFunction = jest.fn();

  • By spying on a function from an external module const mockValidate = jest.spyOn(utils, 'validateString');

  • By mocking a module jest.mock('./utils.js');

  • By creating a __mock__ folder that will replace a whole module's implementation creating a file with the same name as that module's name inside that folder.

jest.fn should be enough for most React components, since you'll probably want to mock functions that are passed as props to them when they are instantiated, like onClick. With that said, this will be the main focus of the article.

When not to use mocks

As magical as mocks are, there is always a limit. They should mostly be used as a secondary tool for testing and should never be used to mock your primary test objective. For example, this does not aggregate any value for your test suite:

test('validateString returns false', () => {
  // mock the validateString function (imported through the utils module)
  const mockedValidateString = jest.spyOn(
    utils, 'validateString'
  ).mockReturnValue(false);
  const result = mockedValidateString();
  // check if it'll return false, as specified by the mock
  expect(result).toBeFalsy();
  mockedValidateString.mockRestore();
});

This will not aggregate anything either:

test('api call returns expected body', () => {
  const expected = [{ id: 1, username: 'user' }];
  // Mocks an API function called getAllUsers,
  // so it won't call the actual API to get the results
  jest.spyOn(api, 'getAllUsers').mockReturnValue(expected);

  // calls a function that will eventually call getAllUsers and return its result
  const result = anotherFunction();
  expect(result).toHaveBeenCalledTimes(1);
  expect(result).toEqual(expected);
});

Both cases do the exact same thing: mock a function, then call it directly and check for its result. Even though there is an obvious redundancy here, sometimes it's not that clear.

Overusing mocks

Another possible problem is the overuse of mocks. If you're not sure of what you should mock, it may be best to not do it at all or you might end up mocking everything you could and then your code will be completely tainted. This usually happens when you use jest.fn/jest.mock/jest.spyOn many times without assigning it to anything, just for the sake of stopping a certain module or function from working, or when you've simply mocked everything you could – and both cases could result in:

  • The code changing so much through mocks that the way it works is completely different and that might yield different results from its original version, even with the same inputs.

  • Mocking a function that is no longer being called because you've also mocked the function that calls it, rending the first function completely useless (this also falls under the redundancy case).

The best way to avoid these cases (both the redundancy and the overuse) is to always be sure that you have a reason for mocking something. So, I'll repeat - if the use of mocks doesn't really aid you in any way to achieve your test's objective, it may be better not to use it at all (or at least not in the way you're using).

Code examples

All of the code examples that appear here come from this repository and the link below each code block takes you to their proper file and line number, as the version of the code in this article was simplified for the sake of readability (see the View on Github links). You can also find more test examples in the repository.

All of the examples will be based on the test cases for the following components:

class Tag extends Component {
  toggleFormOpen() {
    const { isFormOpen } = this.state;
    this.setState({
      isFormOpen: !isFormOpen,
    });
  }

  handleSubmit(e, name) {
    this.setState({ name });
  }

  render() {
    const { isFormOpen, name } = this.state;
    const toggleForm = () => this.toggleFormOpen();
    return isFormOpen ? (
      <TagForm
        name={name}
        onCancel={toggleForm}
        onSubmit={(e, values) => this.handleSubmit(e, values)}
      />
    ) : (
      <button
        className={classnames(styles.tag, styles.clickable)}
        onClick={toggleForm}
        type="button"
      >
        {name}
      </button>
    );
  }
}

[View on Github]

class TagForm extends Component {
  handleSubmit(e) {
    e.preventDefault();
    const { onSubmit, onCancel } = this.props;
    const { value } = this.state;
    if (validateString(value)) {
      onSubmit(e, value);
      onCancel();
    }
  }

  handleChange(e) {
    const { value } = e.target;
    this.setState({ value });
  }

  render() {
    const { onCancel } = this.props;
    const { value } = this.state;
    return (
      <form onSubmit={e => this.handleSubmit(e)}>
        <input
          type="text"
          id="name"
          onChange={e => this.handleChange(e)}
          value={value}
          maxLength={35}
        />
        <button type="button" onClick={onCancel}></button>
      </form>
    );
  }
}

[View on Github]

Mocking with jest.fn

As I've mentioned before, the simplest and most practical way to mock something is by using jest.fn(). This approach is perfect when you have outside access to the function you want to mock. See the example below:

// TagForm.spec.js

test('calls onCancel when ✖ is clicked', () => {
  const mockedOnCancel = jest.fn();

  const wrapper = shallow(<TagForm
    name="test"
    onSubmit={jest.fn()}
    onCancel={mockedOnCancel}
  />);

  wrapper.find('button[type="button"]').at(0).simulate('click');
  expect(mockedOnCancel).toHaveBeenCalledTimes(1);
});

[View on Github]

In this case, the function is passed as a prop to the component, the first being called if a certain event occurs. Since you have the freedom here to pass whatever you want to onCancel, passing a mocked function by using jest.fn() is all that needs to be done.

The purpose of this test is to check whether onCancel is called when the cancel button is clicked. Outside the test scenario, onCancel should close the form without saving the changes and return to its view mode state (the Tag component) - but since we've replace its original function with a mockedOnCancel implementation, that behavior doesn't matter anymore and all we want is the call count of our mock after the click:

// Click the cancel button
wrapper.find('button[type="button"]').at(0).simulate('click');

// Check if onCancel was called exactly 1 time
expect(mockedOnCancel).toHaveBeenCalledTimes(1);

Now, let's say you want to check whether some functions are called when the form fails to submit. In this case you don't want them to be called, so you need to be sure that they won't do so. The purpose here is to check if the form stops the user from saving a tag with an empty name:

test('doesnt call onSubmit/onCancel when the form is submitted and name.length === 0', () => {
  const mockedOnSubmit = jest.fn();
  const mockedOnCancel = jest.fn();

  const wrapper = shallow(<TagForm
    name="test"
    onSubmit={mockedOnSubmit}
    onCancel={mockedOnCancel}
  />);

  wrapper.find('input#name').simulate('change', {
    target: {
      id: 'name',
      value: '',
    },
  });

  wrapper.find('form').simulate('submit');
  expect(mockedOnSubmit).not.toHaveBeenCalled();
  expect(mockedOnCancel).not.toHaveBeenCalled();
});

[View on Github]

There are two functions that are called when the form is submitted: onSubmit which does the actual submitting, and onCancel which will return to the Tag component, but expected changes added.

First you need to simulate the events, so you should change the input value to '' and then simulate the form's submission.

wrapper.find('input#name').simulate('change', {
  target: {
    id: 'name',
    value: '',
  },
});

wrapper.find('form').simulate('submit');

Once the changes have been made, you can call .not.toHaveBeenCalled() for each function to test if they were not called (Jest handles negations by using the .not object, so everything after .not will be negated).

expect(mockedOnSubmit).not.toHaveBeenCalled();
expect(mockedOnCancel).not.toHaveBeenCalled();

So if there's no input to save (as in, if name.trim().length === 0) onCancel and onSubmit should not be called, since the user should not be able to leave the form unless they click on the actual cancel button.

Mocking a component's instance method

Enzyme's API has a method that can be used to access the component's instance object (called .instance()) which can be used to access all of its methods, including the ones you implement inside the component's class. See the code below:

test('calls this.handleChange when the input changes', () => {
  const mockedHandleChange = jest.fn();

  const wrapper = shallow(<TagForm
    name="test"
    onSubmit={jest.fn()}
    onCancel={jest.fn()}
  />);
  wrapper.instance().handleChange = mockedHandleChange;

  ['new value', 'another new value', 'last new value'].forEach((value) => {
    wrapper.find('input#name').simulate('change', {
      target: {
        id: 'name',
        value,
      },
    });
  });
  expect(mockedHandleChange).toHaveBeenCalledTimes(3);
});

[View on Github]

There are three things happening here:

  • We're mocking but not using both onCancel and onSubmit functions.
  • We're mocking the instance's handleChange method.
  • We're testing that method by running a bulk of changes.

If you only want to stop a function from actually running, its mock doesn't necessarily need to be used after its creation, as seen in the example below:

const wrapper = shallow(<TagForm
  name="test"
  onSubmit={jest.fn()}
  onCancel={jest.fn()}
/>);

Always be aware of what you're doing, or you may end up mocking things there are not supposed to be mocked and that may alter or taint your test results.

Now you just need to test if handleChange is being called correctly with the correct amount of times, so the first thing you should do is to mock it:

const mockedHandleChange = jest.fn();
wrapper.instance().handleChange = mockedHandleChange;

But sometimes testing if a mock is called only once may not give you the certainty that you expect from a test. So you can do a bulk of changes and see how handleChange will behave:

['new value', 'another new value', 'last new value'].forEach((value) => {
  wrapper.find('input#name').simulate('change', {
    target: {
      id: 'name',
      value,
    },
  });
});
expect(mockedHandleChange).toHaveBeenCalledTimes(3);

As you can see, three changes (['new value', 'another new value', 'last new value']) result in three different calls to handleChange, which is what we were expecting.

Testing each mock function call separately

Let's consider the logic from the previous test code with some small changes:

test('calls handleChange 5 times with different arguments each time', () => {
  const mockedHandleChange = jest.fn();

  const wrapper = shallow(<TagForm
    name="test"
    onSubmit={jest.fn()}
    onCancel={jest.fn()}
  />);
  wrapper.instance().handleChange = mockedHandleChange;

  const values = [
    'new value 1', 'new value 2', 'new value 3',
    'new value 4', 'new value 5',
  ];
  values.forEach((value) => {
    wrapper.find('input#name').simulate('change', {
      target: {
        id: 'name',
        value,
      },
    });
  });

  expect(mockedHandleChange).toHaveBeenCalledTimes(5);

  const { calls } = mockedHandleChange.mock;
  expect(calls).toHaveLength(5);

  calls.forEach((call, i) => (
    expect(call).toEqual([
      { target: { id: 'name', value: values[i] } },
    ])
  ));
});

[View on Github]

In this case we're testing the exact same function, handleChange, but with a different approach: this time we want to know exactly which argument was given to the function and when. To do so, we can break this forEach loop down to five simple statements:

// For consistency, you have to pass the arguments as a list, even if it's just one argument.
expect(calls[0]).toEqual([{ target: { id: 'name', value: 'new value 1' } }]);
expect(calls[1]).toEqual([{ target: { id: 'name', value: 'new value 2' } }]);
expect(calls[2]).toEqual([{ target: { id: 'name', value: 'new value 3' } }]);
expect(calls[3]).toEqual([{ target: { id: 'name', value: 'new value 4' } }]);
expect(calls[4]).toEqual([{ target: { id: 'name', value: 'new value 5' } }]);

All that these statements do is get the mocked function's nth call and check if it was called with the expected arguments.

Spying with jest.spyOn

Sometimes we need to test a component that calls an external helper function which is imported from another module. But with the way that mocks work, mocking this helper would be a very complicated task if we used jest.fn. Thankfully, jest comes with a very simple solution called spyOn:

import * as utils from 'app/utils';

// some hidden test codes
describe(..., () => {
  test('doesnt submit the form when validateString returns false', () => {
    const mockedValidateString = jest.spyOn(utils, 'validateString').mockReturnValue(false);
    const mockedOnSubmit = jest.fn();
    const wrapper = shallow(<TagForm
      name="test"
      onSubmit={mockedOnSubmit}
      onCancel={jest.fn()}
    />);
    wrapper.find('form').simulate('submit', {
      preventDefault: jest.fn(),
    });

    expect(mockedValidateString).toHaveBeenCalledTimes(1);
    expect(mockedOnSubmit).not.toHaveBeenCalled();
    mockedValidateString.mockRestore();
  });
});

[View on Github]

If we break this down into parts, this is what we get:

import * as utils from 'app/utils';

The whole utils module was imported here just so we can reference it as the first argument of the spyOn method:

const mockedValidateString = jest.spyOn(utils, 'validateString').mockReturnValue(false);

Then, since validateString is a function inside the utils module, we can create a mocked version of it without having to actually pass it to the component, since we are already importing it inside the component's file:

// TagForm.js
import { validateString } from 'app/utils';

So we can just check whatever we need to and then restore the function to its original code, clearing the mock:

expect(mockedValidateString).toHaveBeenCalledTimes(1);
mockedValidateString.mockRestore();

Other mocking techniques

You can do a lot of things when testing with mocks, more than I could possibly present in just one post. Keep in mind though, that Jest's documentation has some pretty good guides to help you out in specific cases, like manual mocks, ES6 class mocks and bypassing module mocks – all three were cited in the Packages and configurations section of this article.

And that's it! Let's recap everything we've learned here:

  • You can use mocks to block or mimic a module/function's behavior.
  • You can use mocks to simulate a module/function's result.
  • You should always be sure that you're not writing redundant test cases with mocks.
  • Don't overuse mocks or your tests may not work in the same way the original code does.
  • If the use of mocks doesn't really aid you in any way to achieve your test's objective, it may be better not to use it at all.
  • There are four basic ways to mock with Jest; we covered jest.fn and jest.spyOn, but you can check the documentation and go for jest.mock or yet a mock file inside a __mock__ folder.
  • There are many ways to approach your tests cases with mocks and it doesn't really matter which one you choose as long as it meets your requirements.
  • Reading Jest's documentation is always a good start, not only with mocks, but with everything test-related.

Coming next

The third part of this article will include test cases with redux and how the approach changes with it.

Thanks to Arimatea Neto, Vanessa Barreiros and Eduardo Cuducos for reviewing this post!

About Victor Ferraz

Fullstack Developer

Comments