Jest spyOn function called

141,670

Solution 1

You were almost done without any changes besides how you spyOn. When you use the spy, you have two options: spyOn the App.prototype, or component component.instance().


const spy = jest.spyOn(Class.prototype, "method")

The order of attaching the spy on the class prototype and rendering (shallow rendering) your instance is important.

const spy = jest.spyOn(App.prototype, "myClickFn");
const instance = shallow(<App />);

The App.prototype bit on the first line there are what you needed to make things work. A JavaScript class doesn't have any of its methods until you instantiate it with new MyClass(), or you dip into the MyClass.prototype. For your particular question, you just needed to spy on the App.prototype method myClickFn.


jest.spyOn(component.instance(), "method")

const component = shallow(<App />);
const spy = jest.spyOn(component.instance(), "myClickFn");

This method requires a shallow/render/mount instance of a React.Component to be available. Essentially spyOn is just looking for something to hijack and shove into a jest.fn(). It could be:

A plain object:

const obj = {a: x => (true)};
const spy = jest.spyOn(obj, "a");

A class:

class Foo {
    bar() {}
}

const nope = jest.spyOn(Foo, "bar");
// THROWS ERROR. Foo has no "bar" method.
// Only an instance of Foo has "bar".
const fooSpy = jest.spyOn(Foo.prototype, "bar");
// Any call to "bar" will trigger this spy; prototype or instance

const fooInstance = new Foo();
const fooInstanceSpy = jest.spyOn(fooInstance, "bar");
// Any call fooInstance makes to "bar" will trigger this spy.

Or a React.Component instance:

const component = shallow(<App />);
/*
component.instance()
-> {myClickFn: f(), render: f(), ...etc}
*/
const spy = jest.spyOn(component.instance(), "myClickFn");

Or a React.Component.prototype:

/*
App.prototype
-> {myClickFn: f(), render: f(), ...etc}
*/
const spy = jest.spyOn(App.prototype, "myClickFn");
// Any call to "myClickFn" from any instance of App will trigger this spy.

I've used and seen both methods. When I have a beforeEach() or beforeAll() block, I might go with the first approach. If I just need a quick spy, I'll use the second. Just mind the order of attaching the spy.


EDIT: If you want to check the side effects of your myClickFn you can just invoke it in a separate test.

const app = shallow(<App />);
app.instance().myClickFn()
/*
Now assert your function does what it is supposed to do...
eg.
expect(app.state("foo")).toEqual("bar");
*/

EDIT: Here is an example of using a functional component. Keep in mind that any methods scoped within your functional component are not available for spying. You would be spying on function props passed into your functional component and testing the invocation of those. This example explores the use of jest.fn() as opposed to jest.spyOn, both of which share the mock function API. While it does not answer the original question, it still provides insight on other techniques that could suit cases indirectly related to the question.

function Component({ myClickFn, items }) {
   const handleClick = (id) => {
       return () => myClickFn(id);
   };
   return (<>
       {items.map(({id, name}) => (
           <div key={id} onClick={handleClick(id)}>{name}</div>
       ))}
   </>);
}

const props = { myClickFn: jest.fn(), items: [/*...{id, name}*/] };
const component = render(<Component {...props} />);
// Do stuff to fire a click event
expect(props.myClickFn).toHaveBeenCalledWith(/*whatever*/);

If a functional component is niladic (no props or arguments) then you can use Jest to spy on any effects you expect from the click method:

import { myAction } from 'src/myActions'
function MyComponent() {
    const dispatch = useDispatch()
    const handleClick = (e) => dispatch(myAction('foobar'))
    return <button onClick={handleClick}>do it</button>
}

// Testing:
const { myAction } = require('src/myActions') // Grab effect actions or whatever file handles the effects.
jest.mock('src/myActions') // Mock the import

// Do the click
expect(myAction).toHaveBeenCalledWith('foobar')

Solution 2

You're almost there. Although I agree with @Alex Young answer about using props for that, you simply need a reference to the instance before trying to spy on the method.

describe('my sweet test', () => {
 it('clicks it', () => {
    const app = shallow(<App />)
    const instance = app.instance()
    const spy = jest.spyOn(instance, 'myClickFunc')

    instance.forceUpdate();    

    const p = app.find('.App-intro')
    p.simulate('click')
    expect(spy).toHaveBeenCalled()
 })
})

Docs: http://airbnb.io/enzyme/docs/api/ShallowWrapper/instance.html

Solution 3

In your test code your are trying to pass App to the spyOn function, but spyOn will only work with objects, not classes. Generally you need to use one of two approaches here:

1) Where the click handler calls a function passed as a prop, e.g.

class App extends Component {

  myClickFunc = () => {
      console.log('clickity clickcty');
      this.props.someCallback();
  }
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className="App-intro" onClick={this.myClickFunc}>
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

You can now pass in a spy function as a prop to the component, and assert that it is called:

describe('my sweet test', () => {
 it('clicks it', () => {
    const spy = jest.fn();
    const app = shallow(<App someCallback={spy} />)
    const p = app.find('.App-intro')
    p.simulate('click')
    expect(spy).toHaveBeenCalled()
 })
})

2) Where the click handler sets some state on the component, e.g.

class App extends Component {
  state = {
      aProperty: 'first'
  }

  myClickFunc = () => {
      console.log('clickity clickcty');
      this.setState({
          aProperty: 'second'
      });
  }
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className="App-intro" onClick={this.myClickFunc}>
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

You can now make assertions about the state of the component, i.e.

describe('my sweet test', () => {
 it('clicks it', () => {
    const app = shallow(<App />)
    const p = app.find('.App-intro')
    p.simulate('click')
    expect(app.state('aProperty')).toEqual('second');
 })
})
Share:
141,670
Max Millington
Author by

Max Millington

My primary interests are JS and the React Ecosystem

Updated on January 19, 2022

Comments

  • Max Millington
    Max Millington over 2 years

    I'm trying to write a simple test for a simple React component, and I want to use Jest to confirm that a function has been called when I simulate a click with enzyme. According to the Jest docs, I should be able to use spyOn to do this: spyOn.

    However, when I try this, I keep getting TypeError: Cannot read property '_isMockFunction' of undefined which I take to mean that my spy is undefined. My code looks like this:

    import React, { Component } from 'react';
    import logo from './logo.svg';
    import './App.css';
    class App extends Component {
    
      myClickFunc = () => {
          console.log('clickity clickcty')
      }
      render() {
        return (
          <div className="App">
            <div className="App-header">
              <img src={logo} className="App-logo" alt="logo" />
              <h2>Welcome to React</h2>
            </div>
            <p className="App-intro" onClick={this.myClickFunc}>
              To get started, edit <code>src/App.js</code> and save to reload.
            </p>
          </div>
        );
      }
    }
    
    export default App;
    

    and in my test file:

    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './App';
    import { shallow, mount, render } from 'enzyme'
    
    describe('my sweet test', () => {
     it('clicks it', () => {
        const spy = jest.spyOn(App, 'myClickFunc')
        const app = shallow(<App />)
        const p = app.find('.App-intro')
        p.simulate('click')
        expect(spy).toHaveBeenCalled()
     })
    })
    

    Anyone have an insight into what I'm doing wrong?

  • Stefan Musarra
    Stefan Musarra over 6 years
    Before the simulate click is called, call forceUpdate to attach the spy function to the instance: instance.forceUpdate()
  • taystack
    taystack over 6 years
    A class is an object. spyOn works with ClassName.prototype
  • taystack
    taystack over 6 years
    @Byrd I'm not sure what you mean. Is jest not working, spyOn not working, or something different? Is your package.json configured correctly for how you are configuring jest? So many questions about your statements.
  • Alex Young
    Alex Young over 6 years
    A class is not an object. In classical OO it is a blueprint for an object, in JavaScript it is a function. typeof (class A {}) === "function" When we instantiate a class we create an object, based on the class' prototype. The prototype of a class is an object, and we could spy on methods if we wanted to. Ultimately, as per my comments under your answer, we want to test the effect of a click handler, not just that it has been called.
  • youngrrrr
    youngrrrr about 6 years
    Strange.. I couldn't get the above working for a similar test but changing the app render method from 'shallow' to 'mount' fixed it. Any ideas why this might've been the fix/Why 'mount' is not also required for this test?
  • 3stacks
    3stacks about 6 years
    @youngrrrr perhaps your function relies on the DOM, which shallow does not product, whereas mount is a full DOM render
  • andy mccullough
    andy mccullough about 6 years
    the missing forceUpdate caught us out... seems strange though, can anyone explain?
  • Alex Young
    Alex Young almost 6 years
    If the question was "How do I use A to do B", but you knew that using C was a better route to achieve A, then it's probably appropriate to answer C. I've no issue with spyOn, but using it to spy on click handlers in React components is a rubbish approach to testing in 99% of situations.
  • taystack
    taystack over 5 years
    @AlexYoung The method being spied is arbitrary. The path to get to the method is arbitrary. The example code had a flaw and it was addressed. Eventually, someone will have a use case for spyOn with components that don't accept props, or pure components without state. Spying a prototype has been 100% successful for me.
  • Erin Drummond
    Erin Drummond about 5 years
    This was not working for me - until I realised that the component under test was being wrapped by a Higher-Order Component. I needed to use app.find("ActualComponent").instance() instead to get the correct instance to monkey-patch the methods of
  • ZAD-Man
    ZAD-Man about 5 years
    Does the code block after "Or a React.Component.prototype" demonstrate something different than the first code block?
  • taystack
    taystack about 5 years
    @ZAD-Man - Yes. Handling a spy for prototypes and instances are slightly different. Do you want to trigger a spy on any instance's call to a method? Use the prototype approach. Do you want to trigger a spy on just a single instance's call to one of its methods? Use the instancing approach.
  • Dehan
    Dehan about 5 years
    Any idea why this works when we force update :O. That is super freaky!
  • Victor Carvalho
    Victor Carvalho almost 4 years
    Can you give an example with a React functional component?
  • taystack
    taystack almost 4 years
    @VictorCarvalho This technique does not lend itself well to functional components. The goal here is to spy on class methods, which functional components do not have. I would suggest researching testing-library/react for your use-case.
  • alejandrosobko
    alejandrosobko almost 3 years
    <class>.prototype!! you saved my day!!