Spying on React class methods


Given the following class component:

// Test.js
import React, { Component } from 'react';

class Test extends Component {
  componentDidMount() {
    this.func();
  }
  func = () => {
    // noop
  };
  render() {
    return null;
  }
}

export default Test;

To verify that componentDidMount was invoked after component was mounted:

// Test.spec.js
import React from 'react';
import { shallow } from 'enzyme';
import Test from './Test';

it('invokes `componentDidMount` when mounted', () => {
  jest.spyOn(Test.prototype, 'componentDidMount');
  shallow(<Test />);
  expect(Test.prototype.componentDidMount).toHaveBeenCalled();
  Test.prototype.componentDidMount.mockRestore();
});

Now what if we want to spy on the class field arrow function?

Could we do something like below?

// Test.spec.js
// ...

it('invokes `func` when mounted', () => {
  jest.spyOn(Test.prototype, 'func');
  shallow(<Test />);
  expect(Test.prototype.func).toHaveBeenCalled();
  Test.prototype.func.mockRestore();
});

Unfortunately, we get a failure:

FAIL  Test.spec.js
 ● calls `func` when mounted

   Cannot spy the func property because it is not a function; undefined given instead

This is because arrow function class properties aren’t found on the class but on the class instance.

So we have 2 options:

  1. Spy on the instance method and explicitly invoke the lifecycle method
  2. Or refactor to bind in constructor instead of arrows for class methods.

Option 1

Spy on the instance method and explicitly call componentDidMount:

// Test.spec.js
// ...

it('calls `func` when mounted', () => {
  const wrapper = shallow(<Test />);
  const instance = wrapper.instance();
  jest.spyOn(instance, 'func');
  instance.componentDidMount();
  expect(instance.func).toHaveBeenCalled();
});

Here the component remains unchanged whereas the test case needs to be updated.

Option 2

Refactor from class properties (Stage 3) to bind in constructor (ES2015):

// Test.js
import React, { Component } from 'react';

class Test extends Component {
  constructor(props) {
    super(props);
    this.func = this.func.bind(this);
  }
  componentDidMount() {
    this.func();
  }
  func() {
    // noop
  };
  render() {
    return null;
  }
}

export default Test;

Here the test case remains unchanged whereas the component is updated.