Как правильно отслеживать метод реагирующего компонента через прототип класса или экземпляр оболочки энзима? - PullRequest
0 голосов
/ 26 апреля 2019

Я пытаюсь утверждать, что метод компонента класса React вызывается, когда я моделирую щелчок, используя Jest и Enzyme. Когда я пытаюсь шпионить за прототипом класса или wrapper.instance(), я получаю Error: Cannot spy the searchBooks property because it is not a function; undefined given instead.

Мои соответствующие зависимости:

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

Я уже пробовал эти опции, которые выдают вышеупомянутую ошибку.

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

Я могу удалить ошибку с помощью следующего, но шпион по-прежнему не вызывается.

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

Ниже мой код.

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

Мой тест

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

В идеале, я должен быть в состоянии сделать что-то вроде Jest SpyOn функцию под названием .

Ответы [ 2 ]

0 голосов
/ 27 апреля 2019

searchBooks является свойством экземпляра ...

... поэтому он не существует, пока не существует экземпляр, и вы можете шпионить за ним только с помощью экземпляра ...

... но оно также напрямую связано с onClick ...

... это означает, что завертывание searchBooks в шпиона не будет иметь никакого эффекта, пока компонент не выполнит повторную визуализацию.

Таким образом, есть два способа исправить это: вызвать searchBooks, используя функцию стрелки, или повторно обработать компонент перед проверкой onClick, чтобы он был привязан к шпиону, а не к исходной функции.

Вот простой пример, демонстрирующий два подхода:

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!
});
0 голосов
/ 26 апреля 2019

Вы можете попробовать использовать шутку вместо шпиона.

const instance = wrapper.instance() as BookSearch
instance.searchBooks = jest.fn()
wrapper.find(`IconButton[data-test="search-button"]`).simulate('click');
expect(instance.searchBooks).toHaveBeenCalled()
...