How do I correctly spyOn a react component's method via the class prototype or the enzyme wrapper instance?
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!
});
Michael
Updated on June 24, 2022Comments
-
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 getError: 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 about 5 yearsThanks for your reply. Replacing the
searchBooks
method with a mock isn't what I am going for. In this test, I want thesearchBooks
method to be executed as usual because I also want to make assertions about things that happen in thesearchBooks
implementation. Namely, that thesearch
function prop doesn't get called if thesearchQuery
state is empty. -
Michael about 5 yearsThanks for the detailed explanation. I tested both of your options and they worked. I opted for re-rendering the component with
wrapper.setState({})
before simulatingonClick
so it is bound to the spy instead of the original function.