2

I want to test an event handler in React using Jest/Jasmine/Enzyme.

MyComponent.js:

import React from 'react';
class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.clickHandler = this.clickHandler.bind(this);
        this.otherMethod  = this.otherMethod .bind(this);
    }

    clickHandler() { this.otherMethod(); }
    otherMethod() {}

    render() { return <div onClick={this.clickHandler}/>; }
}
export default MyComponent;

MyComponent.test.js:

import React from 'react';
import {mount} from 'enzyme';
import MyComponent from './MyComponent';

it('should work', () => {
    const componentWrapper = mount(<MyComponent/>);
    const component = componentWrapper.get(0);

    spyOn(component, 'otherMethod' ).and.callThrough();
    spyOn(component, 'clickHandler').and.callThrough();

    componentWrapper.find('div').simulate('click');

    expect(component.otherMethod ).toHaveBeenCalled(); // works
    expect(component.clickHandler).toHaveBeenCalled(); // does not work
});

In spite of the fact that I think I'm spying on the two component methods identically, one of them (for otherMethod) works while the other (for clickHandler) does not. I clearly am calling clickHandler as otherMethod wouldn't be called if I wasn't, but toHaveBeenCalled isn't being picked up for clickHandler somehow. Why?

I understand that I don't really have to use either .bind(this) or .and.callThrough() on otherMethod but I use both just to treat the two methods identically and using them on otherMethod shouldn't actually make any difference.

This other SO answer states that I have to spy on a function before attaching it as a listener. If this is my problem then I don't know how to resolve it: The spyOn syntax requires the object that the method is a property of (component in this case) but using component requires prior mounting of MyComponent which forces me to attach the listener first.

It may be relevant that my code uses React (and thus I include reactjs as a question tag) but somehow I doubt it.

Community
  • 1
  • 1
Andrew Willems
  • 11,880
  • 10
  • 53
  • 70
  • 1
    The main problem is that you try to spy on internals of your component. Normally you would inject some kind of callback, to the component or make an async call as the result of the click event. This things came from outside of the component and can easily be spied on (callback) or mocked (async call). Maybe your example is simplified, but you should focus on the result of `this.otherMethod()` not that it was called. – Andreas Köberle Feb 03 '17 at 07:50

1 Answers1

2

For this kind of tests, the common path is to call your handler methods on your component instance instead of simulating the event.

You can be sure the onClick prop is test by the React team, what you can check is what happens when "they" call your handler.

This also allows you to use shallow rendering which at least for me is much faster. You can get a reference to the instance easily with the instance() method in your wrapper, and you can then call your handler and spy on whatever you want.

const wrapper = mount(<MyComponent />)
const instance = wrapper.instance()
instance.clickHandler()
//assert whatever

In fact, probably attaching your spies to the instance`s methods (that have already been binded) will do the trick as well. :)

http://airbnb.io/enzyme/docs/api/ReactWrapper/instance.html

Hope it helps!

CharlieBrown
  • 4,143
  • 23
  • 24
  • (1) If you wanted to demonstrate shallow rendering, should you have used `shallow...` instead of `mount...`? (2) I didn't know about `.instance()`. I (apparently) achieved the same thing using `.get(0)` but I think `.instance()` is clearer, so thanks for that. (3) I will definitely think about your suggestion about _what_ to test, not just _how_. However, I'm still wondering why `component.clickHandler` was not called even if it was not really valuable for me to check this in the first place. (4) I don't understand your suggestion to attach spies to the already bound instance method. – Andrew Willems Feb 03 '17 at 19:16
  • I don't think `get` is the same as `instance`. https://github.com/airbnb/enzyme/blob/master/docs/api/ShallowWrapper/get.md / https://github.com/airbnb/enzyme/blob/master/docs/api/ReactWrapper/get.md – CharlieBrown Feb 03 '17 at 19:22
  • In addition, I didn't want to demonstrate anything, simply pointed out that full mounting is not needed and I prefer the shallow way. – CharlieBrown Feb 03 '17 at 19:24
  • You're right, `get` and `instance` are different, so I'll look into that more carefully. They do accomplish the same thing in this example, however. Also, I agree with you in that I prefer shallow mounting if possible. In this case, however, whether in my code or yours, full mounting seems necessary if you want to access a component's methods whether it's an event listener like `changeHandler` or a more "standard" method like `otherMethod`. – Andrew Willems Feb 03 '17 at 19:46
  • 1
    Hi Andrew. No, you can call instance methods with shallow rendering as well. As someone else has pointed out in a comment to your question, you normally inject a function prop to the component and call that prop from an event handler (it may be a Redux action creator, a parent component callback...). To test that you would mount/shallow your component with a fake prop (a spy), then call your instance method like I showed in my answer, and verify your spy (prop) has been called. I've tested hundreds of components that way and when I can't, it's normally a smell that my component is wrong. – CharlieBrown Feb 04 '17 at 08:15
  • I'm learning a lot from you, so thanks for being persistent. It turns out that, with my code example, `get` works with `mount` _but not_ `shallow` (making me initially insist I had to use `mount`) but `instance` works with both `mount` _and_ `shallow` (showing you were right about being able to used the preferred `shallow` and, further, that I've probably been using `get` wrong). Thanks also for the guidance on how to test the instance method. – Andrew Willems Feb 04 '17 at 11:03