Spying on React functional component method with jest and enzyme; Cannot spyOn on a primitive value
Asked Answered
S

3

23

I am trying to test a React component and make sure that when its button gets clicked, the correct method gets invoked. However, when I try to run my test and try to spy on that method, I get the following message:

Error: Cannot spyOn on a primitive value; undefined given

How do I test that when a button is clicked the correct method is invoked? Thanks!

sampleComponent.jsx:

import * as React from 'react';

const SampleComponent = () => {
  const sampleMethod = () => {
    console.log('hello world');
  };

  return <button onClick={sampleMethod} type="button">Click Me</button>;
};

export default SampleComponent;

sampleComponent.test.jsx:

import * as React from 'react';
import { shallow } from 'enzyme';
import SampleComponent from './sample';

test('testing spy', () => {
  const spy = jest.spyOn(SampleComponent.prototype, 'sampleMethod');
  const wrapper = shallow(<SampleComponent />);
  wrapper.find('button').simulate('click');
  expect(spy).toHaveBeenCalled();
});
Scan answered 28/10, 2019 at 22:4 Comment(1)
I would suggest TypeScript for things like this. Test coverage doesn't mean touching lines of code. TS removes 100% of runtime issues (including using functions that don't exist!) You are trying to test React. React does a much better job testing React. I'm guessing you have a side-effect somewhere in your callback. Your test should spy on/mock import the side-effect and assert the call was made on that piece. Your side-effect business logic could then have its own tests without React. You will have a better time testing this way.Pinchbeck
A
17

The error means, the function sampleMethod you defined inside the functional component SampleComponent is not defined in SampleComponent.prototype. So SampleComponent.prototype.sampleMethod is undefined, jest can't spy on a undefined value.

So the correct way to test sampleMethod event handler is like this:

index.spec.tsx:

import React from 'react';
import SampleComponent from './';
import { shallow } from 'enzyme';

describe('SampleComponent', () => {
  test('should handle click correctly', () => {
    const logSpy = jest.spyOn(console, 'log');
    const wrapper = shallow(<SampleComponent></SampleComponent>);
    const button = wrapper.find('button');
    expect(button.text()).toBe('Click Me');
    button.simulate('click');
    expect(logSpy).toBeCalledWith('hello world');
  });
});

We can spy on console.log, to assert it is to be called or not.

Unit test result with 100% coverage:

 PASS  src/react-enzyme-examples/02-react-hooks/index.spec.tsx
  SampleComponent
    ✓ should handle click correctly (19ms)

  console.log node_modules/jest-mock/build/index.js:860
    hello world

-----------|----------|----------|----------|----------|-------------------|
File       |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
-----------|----------|----------|----------|----------|-------------------|
All files  |      100 |      100 |      100 |      100 |                   |
 index.tsx |      100 |      100 |      100 |      100 |                   |
-----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        5.036s

Dependencies version:

"react": "^16.11.0",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.15.1",
"jest": "^24.9.0",
"jest-environment-enzyme": "^7.1.1",
"jest-enzyme": "^7.1.1",
Aitken answered 31/10, 2019 at 6:29 Comment(6)
Is spying const logSpy = jest.spyOn(console, 'log'); a hack for functional components , cant we spy on actual function name that is sampleMethod . Also if we use class components we can spy with function name and not use console.log , right ? I mean the hack is good but I am wondering we cant keep console statements when in productionSungod
Also using shallow gives me error , mount works for meSungod
suppose the sampleMethod above has some other code instead of console.log statement. Could anyone tell how are we supposed to write the spyOn then?Whacking
@karthikreddy You should spyOn the dependencies which sampleMethod uses. For example, if there is a axois.get(...) statement called inside sampleMethod, you should spy on it like this : jest.spyOn(axois, 'get').mockResolvedValue({}). After triggering sampleMethod, you should assert that expect(axois.get).toBeCalled(). console.log just for demonstration purposesAitken
@slideshowp2 that make sense, because we don't really want to know if the method was called, but if the operations inside of the methods was successfully executed. Assertions from Spying on dependencies also assert that the method has been called.Bullivant
@LinDu Aren't we mixing two different use cases here? If we're unit testing sampleMethod then this approach is good. Once that has been tested, we may want to spy on this method itself while testing the Component itself. The question asked was about spying on 'component method', not spying on method's dependencies...Araujo
F
9

sample.js

import * as React from 'react';

export let util = {sampleMethod: null };

const SampleComponent = () => {
  util.sampleMethod = () => {
    console.log('hello world');
  };

  return <button onClick={sampleMethod} type="button">Click Me</button>;
};

export default SampleComponent;

sample.test.js

import { shallow } from 'enzyme';
import SampleComponent, {util} from './sample';

test('testing spy', () => {
  const spy = jest.spyOn( util, 'sampleMethod' );
  const wrapper = shallow(<SampleComponent />);
  wrapper.find('button').simulate('click');
  expect(spy).toHaveBeenCalled(1);
});

I know I'm late to answer but I think this would help some other developers also

Formulate answered 18/8, 2020 at 16:32 Comment(3)
Cannot spy the sampleMethod property because it is not a function; undefined given insteadCloudcapped
I have updated the code, I hope this will work for you.Formulate
return <button onClick={util.sampleMethod} type="button">Click Me</button>;Convenient
N
2

Also searching for a way on spying on a function inside a functional component, it seems just not possible to be done nicley (booo!). I didn't want to spy on a console log, the 2nd suggestion using an object defined outside the fc I was not able to get it working.

I came up with a solution which is also not nice, but simple and may help others with this problem. It is NOT spying on the function, which was asked for, but the outcome is may be close enough for some scenarios.

MyFC.tsx

const MyFC = ({callback}: {callback?:()=>void}) => {
    const handleMyClick = 
        callback // this is the mock fn
        || ()=> console.log("do stuff") // this would be the regular implementation

    return <button onClick={handleMyClick}>Click Me</button>
}

MyFC.test.tsx

it('should be triggered', () => {
    const mockFn = jest.fn();
    const wrapper = mount(<MyFC callback={mockFn}/>);
    wrapper.find('button').simulate('click')
    expect(mockFn).toBeCalled()        
}

with this approach you can at least test if it's called, with what arguments etc If somebody finds a way to do this properly, I would be glad to hear...

Nomination answered 20/6, 2022 at 14:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.