Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

spy on wrapper.instance() method not called when using shallow() #944

Closed
samit4me opened this issue May 16, 2017 · 48 comments
Closed

spy on wrapper.instance() method not called when using shallow() #944

samit4me opened this issue May 16, 2017 · 48 comments

Comments

@samit4me
Copy link
Contributor

samit4me commented May 16, 2017

Testing a component method in isolation can be done via wrapper.instance().method(), see #208.

Testing a user event actually calls that method can be achieved using jest.spyOn() and .simulate().

To the best of my knowledge, you can spy on either the prototype or instance:

  • jest.spyOn(Component.prototype, 'method')
  • jest.spyOn(wrapper.instance(), 'method')

It seems that the prototype spy will work if you render with either shallow() or mount(), but when using the instance, mount() works and shallow() does not.

This would be fine, just use prototype right! Unfortunately, our team is lazy and don't enjoy binding every method to this in the constructor(), so we use class properties to avoid it. The downside of class properties is that they do not appear on the prototype, so we are forced to spy on the instance. This works perfectly if you render with mount() but in unit tests, we always use shallow() so we can test the "unit" in isolation.

Can you see the dilemma? Should shallow() treat spies differently than mount()?

To demonstrate the issue below is a component and the associated test:

class App extends Component {
  constructor(...args) {
    super(...args);
    this.handleButtonClick = this.handleButtonClick.bind(this);
  }

  handleButtonClick(event) {
    // prototype method
  }

  handleAnchorClick = (event) => {
    // class property
  };

  render() {
    return (
      <div>
        <button onClick={this.handleButtonClick}>Click Me!</button>
        <a href="#" onClick={this.handleAnchorClick}>Click Me!</a>
      </div>
    );
  }
}
describe('spy using prototype', () => {
  it('calls "handleButtonClick()" on button click - using prototype', () => {
      const spy = jest.spyOn(App.prototype, 'handleButtonClick');
      const wrapper = shallow(<App />);
      wrapper.find('button').simulate('click', 'using prototype');
      expect(spy).toHaveBeenCalled();
    });

    // FAILS due to class properties not being on prototype
    it('calls "handleAnchorClick()" on anchor click - using prototype', () => {
      const spy = jest.spyOn(App.prototype, 'handleAnchorClick');
      const wrapper = shallow(<App />);
      wrapper.find('a').simulate('click', 'using prototype');
      expect(spy).toHaveBeenCalled();
    });
});

describe('spy using instance with mount', () => {
  it('calls "handleButtonClick()" on button click', () => {
    const wrapper = mount(<App />);
    const spy = jest.spyOn(wrapper.instance(), 'handleButtonClick');
    wrapper.update();
    wrapper.find('button').simulate('click');
    expect(spy).toHaveBeenCalled();
  });

  it('calls "handleAnchorClick()" on button click', () => {
    const wrapper = mount(<App />);
    const spy = jest.spyOn(wrapper.instance(), 'handleAnchorClick');
    wrapper.update();
    wrapper.find('a').simulate('click');
    expect(spy).toHaveBeenCalled();
  });
});

// FAILS due to shallow(), not sure why but mount() works as you can see above
describe('spy using instance with shallow', () => {
  it('calls "handleButtonClick()" on button click', () => {
    const wrapper = shallow(<App />);
    const spy = jest.spyOn(wrapper.instance(), 'handleButtonClick');
    wrapper.update();
    wrapper.find('button').simulate('click');
    expect(spy).toHaveBeenCalled();
  });

  it('calls "handleAnchorClick()" on button click', () => {
    const wrapper = shallow(<App />);
    const spy = jest.spyOn(wrapper.instance(), 'handleAnchorClick');
    wrapper.update();
    wrapper.find('a').simulate('click');
    expect(spy).toHaveBeenCalled();
  });
});

If you want to try it out, take a look at this repo. Any help will be greatly appreciated.

@ljharb
Copy link
Member

ljharb commented May 16, 2017

If you use class properties instead of proper manual this-binding, you're just stuck.

@samit4me
Copy link
Contributor Author

Thanks for the reply 😄 Is there a reason why class properties work with mount() but not with shallow()??? I'm happy to look into this further, do you have any suggestions where to start looking?

@ljharb
Copy link
Member

ljharb commented May 17, 2017

I'm not actually sure why it works with mount. In the shallow case, when the method isn't on the prototype, then before you've spied on it, you've grabbed a reference to it and shoved that into props - which means that sinon can't possibly override that. When you spy on the prototype first, the reference that's grabbed is to the spy, so it works.

@samit4me
Copy link
Contributor Author

Okay cool thanks, that does make a lot of sense 👍

Out of curiosity, I took a peek at the source code and below is what I found.

The instance() method in mount() seems to be using react's getPublicInstance() method, which appears to return an actual reference. So, after you replace a component method (via the instance) and call update() the component prop onClick is bound to the new method, so when you simulate a click event, it calls the new method. This is my understanding anyway, would be great if someone who actually knows could clarify.

const spy = jest.spyOn(wrapper.instance(), 'handleButtonClick'); // replace function via reference
wrapper.update(); // forceUpdate()
wrapper.find('button').simulate('click'); // actually calls the spy function

The instance() method in shallow() returns an instance and when you replace a component method it appears to replace the method (via console.log()) but if you execute the method (from within ShallowWrapper.js) it executes the old method. Furthermore, if you inspect the instance, you will find that the "onClick" prop on the child also has the old method.

I thought if you then call update() it would fix the problem, but it didn't. In fact, update() is internally calling shallowRenderer.getRenderOutput() which from the docs "returns the shallowly rendered output". To my understanding, this is simply returning the same wrapper from when it was first rendered. To change the actual output, I suspect you would need to call shallowRenderer.render() again.

Possible bug?
After seeing this my thoughts were, what is the point of the update() method! I mean it makes sense for mount(), but for shallow() I can't figure out what it does. So I followed the example in this guide and the second expect actually failed, so not sure if this is a new bug in 15.4 or if it ever worked, do you have any knowledge of this?

Documentation?
I have seen quite a few people struggle when it comes to testing component methods, so I was wondering if you would be happy for me to write up some docs? I'm not sure where it would live in the current docs, but maybe a new FAQs section could be added? Really keen to hear your thoughts!

@samit4me
Copy link
Contributor Author

samit4me commented Jun 3, 2017

Would you like a PR adding these examples to the docs?

@ljharb
Copy link
Member

ljharb commented Jun 3, 2017

Yes, sorry, that sounds great.

@samit4me
Copy link
Contributor Author

samit4me commented Jun 7, 2017

I've been trying to follow the follow the contributing guide and I am unable to build the docs. Must have spent a couple of hours on this, but did the following:

  • forked this repo
  • cloned it
  • npm install && npm run react:15
  • npm run docs:watch

It failed on gitbook install with Error: Cannot find module 'internal/fs'.... After following this issue I removed all my node_modules and .npm directories, brew uninstalled node, yarn etc. Then brew installed node 8 but no improvements so uninstalled everything again.

I then installed node@6 and it got a little further but still failed with fs: re-evaluating native module sources is not supported. If you are using the graceful-fs module, please update it to a more recent version. So my enthusiasm has gone for now, but I would still like to add some docs around this.

Does anyone have any tips, can anyone else get the docs to build? If so how?

@jebeck
Copy link

jebeck commented Jun 23, 2017

I'm having a similar issue - still trying to figure out a solution given what you've described @samit4me

OTOH, I've had a bunch of experience working w/GitBook, so maybe I can help debug your install issue? Based on the issue you linked, I assume you're on a Mac. What OS version? And details of your node setup?

@samit4me
Copy link
Contributor Author

@jebeck Any help with GitBook would be awesome 👍. I've tried on macOS 10.12.5 and Win 10 with node versions 6, 7 and 8. Out of curiosity, are you able to get the docs to build based on the instructions in the contributing guide?

@ljharb
Copy link
Member

ljharb commented Jun 25, 2017

The docs only build for me in node 6; they fail in node 7 and 8. @jebeck any help you could provide on that, too, would be great :-)

@samit4me
Copy link
Contributor Author

What OS are you running @ljharb ?

@jebeck
Copy link

jebeck commented Jul 17, 2017

Sorry I've taken so long to look into this. Unfortunately, I'm having the same problems. I just tried a version bump of GitBook up to 3.x (because I'm using that in other projects with newer node versions with zero problems), but that resulted in config errors. I'll open a PR with the config changes, and you all can try it out and see if that's the direction you want to go in. It doesn't work quite yet, looks like there will have to be an awful lot of link rewriting from docs/etc/file.md to just etc/file.md because it's necessary to specify docs/ as the root for GitBook 3.x in order to have it look for the SUMMARY.md and GLOSSARY.md in there.

CC @samit4me

@ljharb
Copy link
Member

ljharb commented Jul 17, 2017

@samit4me normal latest Mac OS.

@jebeck we can give your PR a shot, but in my experience trying to improve our docs generation, gitbook 3 has been harder to work with than gitbook 2.

@samit4me
Copy link
Contributor Author

I just uninstalled all versions of node, npm, yarn etc and installed nvm and then tried on macOS Sierra 10.12.5 with node 6.11.1 and npm 3.10.10. After following the guide it failed on gitbook install with fs: re-evaluating native module sources is not supported. If you are using the graceful-fs module, please update it to a more recent version. Not sure what else to do with that.

I then bumped the version of GitBook to 3.x as per @jebeck PR and it built correctly with the same versions listed above. I don't have any experience with GitBook but it sounds like a good idea to upgrade. Will put this on hold until these GitBook issues are resolved.

@iwllyu
Copy link

iwllyu commented Aug 14, 2017

@samit4me in your examples if you replace wrapper.update() when shallow rendering with wrapper.instance().forceUpdate() the failing tests will work

see: #622

@ljharb

If you use class properties instead of proper manual this-binding, you're just stuck.

I've been following your advice on constructor binding and it's performance issues but it seems to conflict with what they're saying here facebook/react#9851 (comment)

tldr; performance wise, doing a constructor bind is identical to having a class property, with the added benefit of also being on the prototype (and the "performance" hit that comes with it)

@ljharb
Copy link
Member

ljharb commented Aug 14, 2017

That comment is incorrect about them being essentially the same. In practice, the performance might be the same with React, but that doesn't mean they're equivalent choices.

@bchenSyd
Copy link

any updates on this? still the case on latest enzyme and jest

@ljharb
Copy link
Member

ljharb commented Dec 22, 2017

@bochen2014 no update on the difference here between mount and shallow, no

@bchenSyd
Copy link

bchenSyd commented Dec 22, 2017

with latest jest, react and enzyme, if you are using transform-class-properties, here is the behaviour . ( source code can be found here if you want to run locally, don't forget to git checkout enzyme-class-properties after downloading the source code)

// shallow case
it.only('spyOn', () => {
    const wrapper = shallow(<ToggleCheckbox />);
    const spy = jest.spyOn(wrapper.instance(), 'onChange');
    wrapper.instance().forceUpdate();
    const myBtn = wrapper.find('MyButton')
    myBtn.simulate('click');

// fail;  console.log shows that onChange method is called  but assert still fails. why??
    expect(spy).toBeCalled();  
  })

// mount case
it.only('spyOn', () => {
    const wrapper = mount(<ToggleCheckbox />);
    const spy = jest.spyOn(wrapper.instance(), 'onChange');
    wrapper.update();
    const myBtn = wrapper.find('MyButton')
    myBtn.simulate('click');

// fail;  onChange method is NOT called and assertion failed
    expect(spy).toBeCalled();  
  })

(I can confirm that using traditional prototype functions and explicit binding works well with enzyme )

@ljharb
Copy link
Member

ljharb commented Dec 22, 2017

@bochen2014 right; it's simply impossible to spy on the function successfully when it's a class property, because the reference is captured before you spy on it. Don't put functions on class properties, full stop.

@bchenSyd
Copy link

thanks for letting me know ;-)

I can understand that if you do transform-class-properties, those functions are defined at component instance level, rather than its prototype. With that been said, when we do

wrapper.instance().onChange = jest.fn()

aren't we replacing the instance method?

I think @samit4me has done some insight research and find it more like a defect. have you got a chance to look at it?
I think it's a very common use case to have babel-plugin-transform-class-properites . it's currently in stage2 at the moment. It's just a matter of time that it become standard babel

@ljharb
Copy link
Member

ljharb commented Dec 23, 2017

@bochen2014 yes you are; but you're replacing it after a permanent reference to the original is held in the rendered elements.

It's very common, but it's still not a bug - this is how function-valued class properties in react components are supposed to behave, and the only solution is to not use them.

@bchenSyd
Copy link

that makes sense to me.
thanks again for your patience and detailed explanation

@Artimal
Copy link

Artimal commented Jan 5, 2018

@ljharb @samit4me @jebeck @bochen2014
Hi guys, I have a problem with spying my App component too.

describe('<App /> component:', () => {
	let wrapper;
	
	beforeEach(() => {
		const props = {
			scroller: {
				limitScroll: jest.fn(),
				scrollbarY: jest.fn(),
				dot: false
			},
			amalaHistory: {
				basename: ''
			},
			scrollScrolling: jest.fn(),
			scrollEnter: jest.fn(),
			scrollLeave: jest.fn(),
			setDot: jest.fn(),
			scrollScrollbaring: jest.fn()
		};

		wrapper = shallow(<App {...props} />);
	});
	describe('Interaction:', () => {
		it('App shallow: arrowDown() is defined', () => {
			expect(wrapper.instance()).toBeDefined();
                        // expect(wrapper.instance().arrowDown).toBeDefined(); undefined !!!
		});
		it('should call arrowDown()', () => {
			const spy = jest.spyOn(wrapper.instance(), 'arrowDown');
			wrapper.instance().forceUpdate();
			wrapper.find('div#app-wrap').simulate('keyDown', {keyCode: 40});
			expect(spy).toHaveBeenCalled();
		});
	});	

My App render() looks like this (I am using ArrowKeysReact for acessing keyboard arrows and I pass it to arrowDown function):

	render() {
		return (
			<ConnectedRouter history={history} basename={this.props.amalaHistory.basename}>
				<div
				{...ArrowKeysReact.events}
				tabIndex="1"
				id="app-wrap"
				ref="app"
				onWheel={this.scrollHandler.bind(this)}>
					<Sidebar />
					<Scrollbar
					ref="Scrollbar"
					enter={this.hoverIn.bind(this)}
					leave={this.hoverOut.bind(this)}
					dragStart={this.dragStart.bind(this)}
					dragExit={this.dragExit.bind(this)}
					dragMove={this.dragMove.bind(this)}
					limitScroll={this.props.scroller.limitScroll}
					scrollbarY={this.props.scroller.scrollbarY}
					dot={this.props.scroller.dot} />
					<main
					id="main-content"
					ref="scrollWindow">
						<Route path="/" component={Page} />
					</main>
				</div>
			</ConnectedRouter>
		);
	}

And other tests (rendering ones) are working (if you wonder):

<App /> component:
    Rendering:
      √ should render <App /> (10ms)
      √ should render <ConectedRouter />
      √ should render div with id #app-wrap
      √ should render <Sidebar /> (10ms)
      √ should render <Scrollbar />
      √ should render main block with id #main-content (10ms)
      √ should render <Route /> (some Subpage inside)
      √ should <App /> snapshot match
    Interaction:
      √ App shallow: arrowDown() is defined
      × should call arrowDown()

Please for help, I decided to start TDD and now I am stopped from prodcutive work from a few days...

@Artimal
Copy link

Artimal commented Jan 5, 2018

After many experiments, I finally can spy functions that I pass as a props:

		const props = {
			scroller: {
				limitScroll: jest.fn(),
				scrollbarY: jest.fn(),
				dot: false
			},
			amalaHistory: {
				basename: ''
			},
			scrollScrolling: jest.fn(),
			scrollEnter: jest.fn(),
			scrollLeave: jest.fn(),
			setDot: jest.fn(),
			scrollScrollbaring: jest.fn()
		};

		wrapper = shallow(<App {...props} />);

		it('should call props.scrollScrolling()', () => {
			wrapper.find('div#app-wrap').simulate('keyDown', {keyCode: 40});
			expect(wrapper.instance().props.scrollScrolling).toHaveBeenCalled();
		}); // passed !!!

But when it is typical component method that maybe influe to component state and not uses props I still can't pass it by doing this:

		it('should call arrowDown()', () => {
			wrapper.instance().arrowDown = jest.fn();
			wrapper.instance().forceUpdate();
			wrapper.find('div#app-wrap').simulate('keyDown', {keyCode: 40});
			expect(wrapper.instance().arrowDown).toHaveBeenCalled();
		}); // NOT passed...

How can I solve that problem? Pleaase for help and greetings.

@ljharb
Copy link
Member

ljharb commented Jan 5, 2018

@Artimal the only solution to that problem is to NEVER use arrow functions in class fields, to make your arrowDown an instance method, to add this.arrowDown = this.arrowDown.bind(this) to your constructor, and then to spy on App.prototype.arrowDown before you create the wrapper (and creating the wrapper should always be done in an it or a beforeEach, never inside a describe).

@Artimal
Copy link

Artimal commented Jan 5, 2018

@ljharb Thank you for your response.

  1. I create wrapper in beforeEach of course by shallow with props that turn standard props :)
  2. arrowDown is an App method like below. How do you see that strange (for me) "this.arrowDown = this.arrowDown.bind(this)"? What it will give?
export class App extends React.Component {
	constructor (props) {
		super(props);
		ArrowKeysReact.config({
			up: this.arrowUp.bind(this),
			down: this.arrowDown.bind(this)
		});
	}

	arrowDown() {
		this.props.scrollScrolling({
			delta: 1,
			scale: 0.3
		});		
	}
(...)
  1. What do you mean by "NEVER use arrow functions in class fields"? Can you show me exact parts of my code when I do that mistake?
  2. I do not shallow before but I use wrapper update. Following 2 test are ending with same error: "expect(jest.fn()).toHaveBeenCalled() Expected mock function to have been called.":
		it('should call arrowDown()', () => {
			wrapper.instance().arrowDown = jest.fn();
			wrapper.update();
			wrapper.find('div#app-wrap').simulate('keyDown', {keyCode: 40});
			expect(wrapper.instance().arrowDown).toHaveBeenCalled();
		});
		it('should call arrowDown() BUT with spyOn', () => {
			const spy = jest.spyOn(App.prototype, "arrowDown");
			wrapper.update();
			wrapper.find('div#app-wrap').simulate('keyDown', {keyCode: 40});
			expect(spy).toHaveBeenCalled();
		});

@ljharb
Copy link
Member

ljharb commented Jan 5, 2018

OK, clearly my guesses about your App implementation are wrong :-) Re number 4: you do need to spy on the prototype before the wrapper is created. Meaning, you probably need to create the wrapper directly inside the it (which I'd always recommend anyways).

@hmorgancode
Copy link

hmorgancode commented Jan 31, 2018

An alternate approach that also works: Instead of assigning your function directly as the event handler, call it within an anonymous function. The anonymous function will dereference this.yourFunction at runtime, instead of following a permanently-assigned reference as explained above.

Let's assume we have an arbitrary onSubmit arrow function on our class. With this test code:

const spyOnSubmit = jest.spyOn(wrapper.instance(), 'onSubmit');
wrapper.find(Button).simulate('click');
expect(spyOnSubmit).toHaveBeenCalled();

...this will pass:

<Button
  onClick={() => this.onSubmit()} // calls spyOnSubmit!
/>

...and this will fail:

<Button
  onClick={this.onSubmit} // calls original onSubmit, NOT our spy.
/>

The tradeoff here is that we're using an arrow function as our handler- it'll be recreated with each render, which is a very minor penalty but still one to consider.

@ljharb
Copy link
Member

ljharb commented Feb 1, 2018

@hmorgancode this has huge performance implications, especially when passed into a PureComponent. The best practice remains, use an instance method that's constructor-bound.

@hmorgancode
Copy link

@ljharb oh, woah, I totally blanked on the function equality aspect of this. My bad!

To clarify, you're referring mainly to the fact that each time an arrow function is 'recreated' in render, it's:

  • an entirely new entity in memory, and so...
  • counts as a 'new' function for equality purposes, and so...
  • will cause any PureComponent it's passed to, to re-render unnecessarily?

@ljharb
Copy link
Member

ljharb commented Feb 1, 2018

That's correct. You can avoid that by using either a constructor-bound instance method, or an arrow function in a class property. However, an arrow function in a class property has the downside of being less optimizeable and less testable.

@sstern6
Copy link
Contributor

sstern6 commented May 9, 2018

@ljharb why would arrow functions on a class be less optimizable? Would it be bc the arrow function on the class would not have a reference to the name of the function and would keep re creating new functions? Though, when using class bound functions it will be able to reference the function name, thus not recreating new functions?

Thanks

@dvakatsiienko
Copy link

@iwllyu damn hell thank you

Your advice with result.instance().forceUpdate() worked as a charm.

@ljharb
Copy link
Member

ljharb commented Jul 4, 2018

@sstern6 as we've discussed separately, the "meat" of the function would live on the prototype, which would get easily optimized - and then the only piece that repeats N times is the trivial bind-proxy.

@ljharb
Copy link
Member

ljharb commented Jul 4, 2018

This seems answered; the solution remains "never use arrow functions in class properties".

@ljharb ljharb closed this as completed Jul 4, 2018
@biin
Copy link

biin commented Aug 1, 2018

@iwllyu Thank you.
You save my life. result.instance().forceUpdate() worked as a charm.

@alanyinjs
Copy link

alanyinjs commented Dec 23, 2018

Hi @ljharb ,

I was wondering if you could please expand on your previous comment:

it's simply impossible to spy on the function successfully when it's a class property, because the reference is captured before you spy on it.

I am not sure if I completely understood it. I understood that class properties are defined on the component instance rather than its prototype. However I don't quite get why it could not be overridden by a mock if we do const methodSpy = jest.spyOn(wrapper.instance(), 'classMethodName'). I don't quite get this comment (esp. the 'reference' part):

In the shallow case, when the method isn't on the prototype, then before you've spied on it, you've grabbed a reference to it and shoved that into props - which means that sinon can't possibly override that.

Could you please point me in the right direction? Thanks a lot for your help!

@ljharb
Copy link
Member

ljharb commented Dec 24, 2018

Because the unspied version will have already been passed as a prop into the render tree. The spy afterwards has no effect.

In other words, never put an arrow function in any class field ever - instead, make a normal instance method, and do foo = this.foo.bind(this) in a class field, or this.foo = this.foo.bind(this) in the constructor.

@alanyinjs
Copy link

Thanks a lot for the explanation @ljharb !

I am assuming the reason that this only happens to arrow function class fields but not to to normal class field is due to the following reason:

Each time you pass the reference to the arrow function class field into the render tree , a new arrow function instance is created for the render tree to use (which has no reference to the instance method). When we spy on the instance method, we are only spying the original version of the method, not the one that has been passed in. So basically the two copies don't point to the same reference as normal functions would.

On the other hand, using a normal class method (or binding it in the constructor), we are not creating a new instance of the method, but rather passing in the reference. Therefore by spying on the instance method, we are also changing what has been passed into the render tree as they are connected by reference.

Am I correct?

@ljharb
Copy link
Member

ljharb commented Dec 24, 2018

@alanyinjs it would happen on normal class fields too if you assign them to any non-primitive, but people don't tend to try to replace non-functions with spies.

Your analysis seems correct (noting that you'll need to spy on the instance method before any instance is created).

@alanyinjs
Copy link

alanyinjs commented Dec 25, 2018

@ljharb Yeah you're right...

Just one more question: I still haven't got why spying wrapper.instance().methodName where the method is an arrow function would fail while spying on a class field created by binding a normal function in the constructor would not fail... I thought they would be equivalent except that one is using an arrow function and the other is using a normal function (as Function.prototype.bind() also returns a new function).

I get why jest.spyOn(componentName.prototype, 'methodName') would succeed in the binding case. As we are spying directly on the prototype, therefore each time an instance is created, the reference also points to the prototype's method, which has been spied on. This works well.

However what I don't get is why using normal function and binding would not fail but arrow function class fields would. As binding in the constructor this.className = this.className.bind(this) returns a new function and set it to the class field, while doing className = () => {/* whatever */} (outside the constructor, of course) also returns a new instance of an arrow function and set it to the class field.

Thanks for answering my questions.

@ljharb
Copy link
Member

ljharb commented Dec 25, 2018

The only way to make it work is to spy on the method before the render method is called. When rendering a component with enzyme, the render method is called immediately - so by the time wrapper exists, the unspied arrow function (or bound function) is already sent into the render tree.

@nstfkc
Copy link

nstfkc commented Jan 17, 2019

Here is a simple example, i hope it helps.

class App extends React.Component {
  componentDidMount(){
     this.handleSomething();
  }
  handleSomething = () => {
     //
  }

  ...
}
const wrapper = shallow(<Component />, { disableLifecycleMethods: true })
const instance = wrapper.instance();
const handleSomethingSpy = jest.spyOn(instance, 'handleSomething')

instance.componentDidMount();
expect(handleSomethingSpy).toHaveBeenCalled()

@ljharb
Copy link
Member

ljharb commented Jan 17, 2019

@enestufekci that should work in this case because this.handleSomething isn't passed into the render tree, but there's zero reason it should be an arrow function if it's not. Again, never put arrow functions in class fields - use constructor-bound or field-bound instance methods.

@k-funk
Copy link

k-funk commented May 2, 2020

Everything I'm reading here makes me think that my code here should NOT work, but it does. I wonder if there's some new kinda magic happening under the hood, because I recall having the problem that others have been having in the past, but not now.

node: 14.0.0
@babel/core: 7.9.0
babel-jest: 25.4.0
jest: 25.4.0
enzyme: 16.13.1
react: 16.13.1

index.jsx

export default class DarkLightModeSelector extends PureComponent {
  ...
  setMode = mode => {
    ...
  }

  render() {
    ...
    return (
      <>
          ...
          <Button onClick={() => this.setMode('dark-mode')}>
            Dark
          </Button>
          <Button onClick={() => this.setMode('light-mode')}>
            Light
          </Button>
          ...
      </>
    );
  }
}

index.test.jsx

test('button clicks call setMode', () => {
  const wrapper = shallow((
    <DarkLightModeSelector />
  ));
  const instance = wrapper.instance();
    
  const spy = jest.spyOn(instance, 'setMode');

  wrapper.find('Button').at(0).simulate('click');
  expect(spy).toHaveBeenCalledWith('dark-mode');

  wrapper.find('Button').at(1).simulate('click');
  expect(spy).toHaveBeenCalledWith('light-mode');
});

@bhargav11-crest
Copy link

bhargav11-crest commented Aug 26, 2020

My test :

describe('Personal Profile', () => {
  
  it('renders', () => {
    
    const wrapper = mount(
      <PersonalProfile store={store}/>
    ); 
    const spy = jest.spyOn(wrapper.instance(), 'handleChangetype')
    wrapper.update();
    wrapper.find(Typeahead).at(2).simulate('change');
    console.log(wrapper.find(Typeahead).at(2).simulate('change').debug())
    
    expect(spy).toHaveBeenCalled();
  });
});

I am getting error as below:
image

In my js file, I have not used arrow functions and bound the method in the constructor though I m having this error.

can anybody help ?

@ljharb
Copy link
Member

ljharb commented Sep 1, 2020

@bhargav11-crest you can't spy on the instance after making the wrapper - you need to spy on PersonalProfile.prototype.handleChangeType before creating the wrapper in the first place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests