How do I correctly spyOn a react component's method via the class prototype or the enzyme wrapper instance?

12,355

searchBooks is an instance property...

...so it doesn't exist until the instance exists, and you can only spy on it using the instance...

...but it is also bound directly to onClick...

...which means that wrapping searchBooks in a spy won't have any effect until the component re-renders.

So the two options to fix it are to call searchBooks using an arrow function, or re-rendering the component before testing onClick so it is bound to the spy instead of the original function.

Here is a simple example demonstrating the two approaches:

import * as React from 'react';
import { shallow } from 'enzyme';

class MyComponent extends React.Component {
  func1 = () => { }  // <= instance property
  func2 = () => { }  // <= instance property
  render() { return (
    <div>
      <button id='one' onClick={this.func1}>directly bound</button>
      <button id='two' onClick={() => { this.func2() }}>arrow function</button>
    </div>
  ); }
}

test('click', () => {
  const wrapper = shallow<MyComponent>(<MyComponent/>);
  const spy1 = jest.spyOn(wrapper.instance(), 'func1');
  const spy2 = jest.spyOn(wrapper.instance(), 'func2');

  wrapper.find('#one').simulate('click');
  expect(spy1).not.toHaveBeenCalled();  // Success!  (onClick NOT bound to spy)
  wrapper.find('#two').simulate('click');
  expect(spy2).toHaveBeenCalledTimes(1);  // Success!

  wrapper.setState({});  // <= force re-render (sometimes calling wrapper.update isn't enough)

  wrapper.find('#one').simulate('click');
  expect(spy1).toHaveBeenCalledTimes(1);  // Success!  (onClick IS bound to spy)
  wrapper.find('#two').simulate('click');
  expect(spy2).toHaveBeenCalledTimes(2);  // Success!
});
Share:
12,355
Michael
Author by

Michael

Updated on June 24, 2022

Comments

  • Michael
    Michael almost 2 years

    I am trying to assert that a React class component method is called when I simulate a click using Jest and Enzyme. When I try to spyOn the class prototype or wrapper.instance() I get Error: Cannot spy the searchBooks property because it is not a function; undefined given instead.

    My relevant dependencies:

    "devDependencies": {
       "enzyme": "^3.9.0"
       "enzyme-adapter-react-16": "^1.6.0",
       "jest": "^23.6.0",
       "jest-enzyme": "^7.0.1",
       "ts-jest": "^23.10.3",
       "typescript": "^3.1.1",
    
      ...
    
    "dependencies": {
       "@material-ui/core": "^3.2.0",
       "@material-ui/icons": "^3.0.1",
       "@material-ui/lab": "^3.0.0-alpha.23",
       "react": "^16.5.2",
    
    

    I have already tried these options, which throw the previously mentioned error.

     let spy = jest.spyOn(wrapper.instance() as MyComponentClass, 'methodName');
     let spy2 = jest.spyOn(MyComponentClass.prototype, 'methodName');
    

    I can remove the error with the following, but the spy still doesn't get called.

    let spy3 = jest.spyOn(wrapper.find(MyComponentClass).instance() as MyComponentClass, 'methodName');
    

    Below is my code.

    import * as React from 'react';
    import { Fragment, Component, ChangeEvent } from 'react';
    import { AudioType } from '../../model/audio';
    import withStyles,  { WithStyles } from '@material-ui/core/styles/withStyles';
    import Typography from '@material-ui/core/Typography';
    import TextField from '@material-ui/core/TextField';
    import BookSearchStyles from './BookSearchStyles';
    import BookDetail from './BookDetail';
    import SearchIcon from '@material-ui/icons/Search';
    import IconButton from '@material-ui/core/IconButton';
    import { VolumeInfo } from '../../model/volume';
    
    export interface BookSearchProps extends WithStyles<typeof BookSearchStyles> {
        search?: (query: string) => void;
    }
    
    export interface BookSearchState {
        searchQuery?: string;
    }
    
    export class BookSearch extends Component<BookSearchProps, BookSearchState> {
        state: BookSearchState = {
            searchQuery: '',
        };
    
        handleChange = (event: ChangeEvent<HTMLInputElement>) => {
            this.setState({
                [event.target.name]: event.target.value,
            });
        };
    
        searchBooks = () => {
            if (this.state.searchQuery) {
                this.props.search(this.state.searchQuery);
            }
        };
    
        render() {
            const { classes, volumes } = this.props;
            return (
                <Fragment>
                    <div className={classes.searchPrompt}>
                        <form className={classes.formContainer}>
                            <div className={classes.search}>
                                <TextField
                                    id="query-text-field"
                                    name="searchQuery"
                                    label="enter book title or author"
                                    className={classes.textField}
                                    value={this.state.searchQuery}
                                    onChange={this.handleChange}
                                    fullWidth={true}
                                />
                                {
                                    <IconButton
                                        onClick={this.searchBooks}
                                        data-test="search-button"
                                    >
                                        <SearchIcon />
                                    </IconButton>
                                }
                            </div>
                        </form>
                    </div>
                </Fragment>
            );
        }
    }
    
    export default withStyles(BookSearchStyles)(BookSearch);
    
    

    My Test

    import * as React from 'react';
    import { mount, ReactWrapper } from 'enzyme';
    import BookSearchWrapped from './index';
    import { BookSearch, BookSearchProps, BookSearchState } from './index';
    
    describe("<BookSearch />", () => {
    
        let wrapper: ReactWrapper<BookSearchProps, BookSearchState, BookSearch>;
        let component: ReactWrapper<BookSearchProps, BookSearchState>;
        let search: (query: string) => void;
    
        beforeEach(() => {
            search = jest.fn();
            wrapper = mount(
                <BookSearchWrapped
                    search={search}
                />
            );
            component = wrapper.find(BookSearch);
        });
    
        it('renders successfully', () => {
            expect(wrapper.exists()).toBe(true);
        });
    
        it("doesn't call props.search() function when the query string is empty", () => {
            let spy = jest.spyOn(wrapper.instance() as BookSearch, 'searchBooks'); //THROWS ERROR
            let spy2 = jest.spyOn(BookSearch.prototype, 'searchBooks'); //THROWS ERROR
            let spy3 = jest.spyOn(component.instance() as BookSearch, 'searchBooks'); //NOT CALLED
    
            wrapper.find(`IconButton[data-test="search-button"]`).simulate('click');
            expect(spy).toHaveBeenCalled();
            expect(spy2).toHaveBeenCalled();
            expect(spy3).toHaveBeenCalled();
            expect(search).not.toHaveBeenCalled();
        });
    });
    

    Ideally, I should be able to do something like Jest spyOn function called.

  • Michael
    Michael about 5 years
    Thanks for your reply. Replacing the searchBooks method with a mock isn't what I am going for. In this test, I want the searchBooks method to be executed as usual because I also want to make assertions about things that happen in the searchBooks implementation. Namely, that the search function prop doesn't get called if the searchQuery state is empty.
  • Michael
    Michael about 5 years
    Thanks for the detailed explanation. I tested both of your options and they worked. I opted for re-rendering the component with wrapper.setState({}) before simulating onClick so it is bound to the spy instead of the original function.