Jimmy
Jimmy

Reputation: 3880

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

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();
});

Upvotes: 27

Views: 38699

Answers (4)

Isaiah Shiner
Isaiah Shiner

Reputation: 525

It seems like the exact example you gave requires:

  • Spy on an inner function of a function component

  • Without mocking the whole component

I'm not sure that's possible, and you probably shouldn't try to test things this way. But here are some workarounds I found helpful for this issue.

Spy on an entire component without mocking it

import SampleComponent from './sample';
import * as SampleComponentModule from './sample';

test('testing render', () => {
  const componentSpy = jest.spyOn(SampleComponentModule, 'default');
  const wrapper = shallow(<SampleComponent />);
  expect(componentSpy).toHaveBeenCalled();
});

Spy on an inner function by mocking the whole component

import SampleComponent from './sample';
// must start with 'mock' to use in mock method
const mockSample = jest.fn();

jest.mock('./sample', () => {
  return {
    __esModule: true,
    default: () => {
      // must re-create functionality, probably a bad idea
      return <button onClick={mockSample} type="button">Click Me</button>;
    }
  }
});

test('testing button click', () => {
  const wrapper = shallow(<SampleComponent />);
  const button = wrapper.find('button');
  button.simulate('click');
  // use the jest.fn directly, no need to make a new spy
  expect(mockSample).toHaveBeenCalled();
});

Why can't I test this?

Most likely, sampleFunction is causing something to happen on the screen that the user can see. If that's the case you should test that instead. You could make sure a div changes text, or another component appears.

If sampleFunction is calculating business logic, you should pull it out into it's own function and test it directly. If it's doing networking, you should spyOn the networking library.

Upvotes: 0

o-faro
o-faro

Reputation: 1031

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...

Upvotes: 2

Murtaza Huzaifa
Murtaza Huzaifa

Reputation: 147

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

Upvotes: 9

Lin Du
Lin Du

Reputation: 102672

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",

Upvotes: 17

Related Questions