Ich teste meine erste App und habe Probleme beim Testen einer mit Redux verbundenen Komponente.

Genauer gesagt teste ich Search.js. Die Idee ist, eine Formularübermittlung in der untergeordneten Komponente DisplaySearcgBar.js zu simulieren und dann zu testen, ob setAlert und getRestaurants aufgerufen werden.

In Test 3 sollte Search.js OnSubmit() aufrufen, der setAlert aufrufen soll, und in # 4 sollte getRestaurants aufgerufen werden, da Eingaben bereitgestellt werden .

Beide Tests werden mit demselben Fehler abgelehnt:

Search › 3 - setAlert called if search button is pressed with no input

    expect(jest.fn()).toHaveBeenCalled()

    Expected number of calls: >= 1
    Received number of calls:    0

      37 |     wrapper.find('[data-test="search"]').simulate('click');
      38 |     //expect(store.getActions().length).toBe(1);
    > 39 |     expect(wrapper.props().children.props.props.setAlert).toHaveBeenCalled();
         |                                                           ^
      40 |   });
      41 | 
      42 |   test('4 - getRestaurant called when inputs filled and search button clicked ', () => {

      at Object.<anonymous> (src/Components/restaurants/Search/__tests__/Search.test.js:39:59)

  ● Search › 4 - getRestaurant called when inputs filled and search button clicked 

    expect(jest.fn()).toHaveBeenCalled()

    Expected number of calls: >= 1
    Received number of calls:    0

      55 |     wrapper.find('[data-test="search"]').simulate('click');
      56 | 
    > 57 |     expect(wrapper.props().children.props.props.getRestaurants).toHaveBeenCalled();
         |                                                                 ^
      58 |   });
      59 | });
      60 | 

      at Object.<anonymous> (src/Components/restaurants/Search/__tests__/Search.test.js:57:65)

Ich bin neu im Testen und ich bin mir nicht sicher, was ich falsch mache.

Ich habe verschiedene Ansätze zur Auswahl beider Funktionen ausprobiert, aber entweder habe ich oben den gleichen Fehler erhalten oder er konnte sie nicht finden. Ich fühle mich wie im Kreis, ich muss etwas vermissen, aber ich verstehe nicht was.

Hier ist Search.test.js

import React from 'react';
import { mount } from 'enzyme';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';

import Search from './../Search';
import DisplaySearchBar from '../../../layout/DisplaySearchBar/DisplaySearchBar';

const mockStore = configureStore([thunk]);
const initialState = {
  restaurants: { restaurants: ['foo'], alert: null },
};
const store = mockStore(initialState);
const mockSetAlert = jest.fn();
const mockGetRestaurants = jest.fn();
const onSubmit = jest.fn();
const wrapper = mount(
  <Provider store={store}>
    <Search setAlert={mockSetAlert} getRestaurants={mockGetRestaurants} />
  </Provider>
);

describe('Search', () => {
  /* beforeEach(() => {
    const form = wrapper.find('form').first();
    form.simulate('submit', {
      preventDefault: () => {},
    });
  }); */

  afterEach(() => {
    jest.clearAllMocks();
  });

  test('1 - renders without errors', () => {
    expect(wrapper.find(DisplaySearchBar)).toHaveLength(1);
  });

  test('2 - if restaurants clearButton is rendered', () => {
    expect(wrapper.find('[data-test="clear"]')).toBeTruthy();
  });

  test('3 - setAlert called if search button is pressed with no input', () => {
    wrapper.find('form').simulate('submit', { preventDefault: () => {} });

    expect(mockSetAlert).toHaveBeenCalled();
  });

  test('4 - getRestaurant called when inputs filled and search button clicked ', () => {
    wrapper
      .find('[name="where"]')
      .at(0)
      .simulate('change', { target: { value: 'foo' } });

    wrapper
      .find('[name="what"]')
      .at(0)
      .simulate('change', { target: { value: 'foo' } });

    wrapper
      .find('[data-test="best_match"]')
      .at(0)
      .simulate('click');

    wrapper.find('form').simulate('submit', { preventDefault: () => {} });

    expect(mockGetRestaurants).toHaveBeenCalledWith({
      name: 'foo',
      where: 'foo',
      sortBy: 'best_match',
    });
  });
});

Search.js

import React, { useState } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';

import { handleScriptLoad } from '../../../helpers/Autocomplete';
import { getRestaurants, setAlert } from '../../../actions/restaurantAction';
import DisplaySearchBar from '../../layout/DisplaySearchBar/DisplaySearchBar';

import styles from './Search.module.scss';

const Search = ({ getRestaurants, setAlert }) => {
  const [where, setWhere] = useState('');
  const [what, setWhat] = useState('');
  const [sortBy, setSortBy] = useState('rating');

  const sortByOptions = {
    'Highest Rated': 'rating',
    'Best Match': 'best_match',
    'Most Reviewed': 'review_count',
  };

  // give active class to option selected
  const getSortByClass = (sortByOption) => {
    if (sortBy === sortByOption) {
      return styles.active;
    } else {
      return '';
    }
  };

  // set the state of a sorting option
  const handleSortByChange = (sortByOption) => {
    setSortBy(sortByOption);
  };

  //handle input changes
  const handleChange = (e) => {
    if (e.target.name === 'what') {
      setWhat(e.target.value);
    } else if (e.target.name === 'where') {
      setWhere(e.target.value);
    }
  };

  const onSubmit = (e) => {
    e.preventDefault();
    if (where && what) {
      getRestaurants({ where, what, sortBy });
      setWhere('');
      setWhat('');
      setSortBy('best_match');
    } else {
      setAlert('Please fill all the inputs');
    }
  };

  // displays sort options
  const renderSortByOptions = () => {
    return Object.keys(sortByOptions).map((sortByOption) => {
      let sortByOptionValue = sortByOptions[sortByOption];
      return (
        <li
          className={`${sortByOptionValue} ${getSortByClass(
            sortByOptionValue
          )}`}
          data-test={sortByOptionValue}
          key={sortByOptionValue}
          onClick={() => handleSortByChange(sortByOptionValue)}
        >
          {sortByOption}
        </li>
      );
    });
  };

  return (
    <DisplaySearchBar
      onSubmit={onSubmit}
      handleChange={handleChange}
      renderSortByOptions={renderSortByOptions}
      where={where}
      what={what}
      handleScriptLoad={handleScriptLoad}
    />
  );
};

Search.propTypes = {
  getRestaurants: PropTypes.func.isRequired,
  setAlert: PropTypes.func.isRequired,
};

export default connect(null, { getRestaurants, setAlert })(Search);

Seine untergeordnete Komponente, in der sich die Schaltfläche befindet

import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { clearSearch } from '../../../actions/restaurantAction';
//Import React Script Libraray to load Google object
import Script from 'react-load-script';
import Fade from 'react-reveal/Fade';
import Alert from '../Alert/Alert';

import styles from './DisplaySearchBar.module.scss';

const DisplaySearchBar = ({
  renderSortByOptions,
  onSubmit,
  where,
  handleChange,
  what,
  handleScriptLoad,
  restaurants,
  clearSearch,
}) => {
  const googleUrl = `https://maps.googleapis.com/maps/api/js?key=${process.env.REACT_APP_GOOGLE_API_KEY}&libraries=places`;
  // {googleUrl && <Script url={googleUrl} onLoad={handleScriptLoad} />}
  return (
    <section className={styles.searchBar}>
      <form onSubmit={onSubmit} className={styles.searchBarForm}>
        <legend className="title">
          <Fade left>
            <h1>Where are you going to eat tonight?</h1>
          </Fade>
        </legend>
        <Fade>
          <fieldset className={styles.searchBarInput}>
            <input
              type="text"
              name="where"
              placeholder="Where do you want to eat?"
              value={where}
              onChange={handleChange}
              id="autocomplete"
            />

            <input
              type="text"
              name="what"
              placeholder="What do you want to eat?"
              onChange={handleChange}
              value={what}
            />
            <div data-test="alert-holder" className={styles.alertHolder}>
              <Alert />
            </div>
          </fieldset>

          <fieldset className={styles.searchBarSubmit}>
            <input
              data-test="search"
              className={`${styles.myButton} button`}
              type="submit"
              name="submit"
              value="Search"
            ></input>

            {restaurants.length > 0 && (
              <button
                data-test="clear"
                className={`${styles.clearButton} button`}
                onClick={clearSearch}
              >
                Clear
              </button>
            )}
          </fieldset>
        </Fade>
      </form>
      <article className={styles.searchBarSortOptions}>
        <Fade>
          <ul>{renderSortByOptions()}</ul>
        </Fade>
      </article>
    </section>
  );
};

DisplaySearchBar.propTypes = {
  renderSortByOptions: PropTypes.func.isRequired,
  where: PropTypes.string.isRequired,
  handleChange: PropTypes.func.isRequired,
  what: PropTypes.string.isRequired,
  handleScriptLoad: PropTypes.func.isRequired,
  restaurants: PropTypes.array.isRequired,
  clearSearch: PropTypes.func.isRequired,
};

const mapStatetoProps = (state) => ({
  restaurants: state.restaurants.restaurants,
});

export default connect(mapStatetoProps, { clearSearch })(DisplaySearchBar);

RestaurantActions.js

import { getCurrentPosition } from '../helpers/GeoLocation';
import {
  getRestaurantsHelper,
  getRestaurantsInfoHelper,
  getDefaultRestaurantsHelper,
} from '../helpers/utils';

import {
  CLEAR_SEARCH,
  SET_LOADING,
  GET_LOCATION,
  SET_ALERT,
  REMOVE_ALERT,
} from './types';

// Get Restaurants
export const getRestaurants = (text) => async (dispatch) => {
  dispatch(setLoading());

  getRestaurantsHelper(text, dispatch);
};

// Get Restaurants Info
export const getRestaurantInfo = (id) => async (dispatch) => {
  dispatch(setLoading());
  getRestaurantsInfoHelper(id, dispatch);
};

// Get default restaurants
export const getDefaultRestaurants = (location, type) => async (dispatch) => {
  if (location.length > 0) {
    getDefaultRestaurantsHelper(location, type, dispatch);
  }
};

// Get location
export const fetchCoordinates = () => async (dispatch) => {
  try {
    const { coords } = await getCurrentPosition();
    dispatch({
      type: GET_LOCATION,
      payload: [coords.latitude.toFixed(5), coords.longitude.toFixed(5)],
    });
  } catch (error) {
    dispatch(setAlert('Location not available'));
  }
};

// Set loading
export const setLoading = () => ({ type: SET_LOADING });

// Clear search
export const clearSearch = () => ({ type: CLEAR_SEARCH });

// Set alert
export const setAlert = (msg, type) => (dispatch) => {
  dispatch({
    type: SET_ALERT,
    payload: { msg, type },
  });

  setTimeout(() => dispatch({ type: REMOVE_ALERT }), 5000);
};

Hier ist das vollständige Repository auf Github: https://github.com/mugg84/RestaurantFinderRedux.git

Vielen Dank im Voraus für Ihre Hilfe!!

0
Mugg84 14 Aug. 2020 im 14:34

3 Antworten

Beste Antwort

Ich glaube, ich habe herausgefunden, wie man testet, ob setAlert und getRestaurants aufgerufen werden. Ich habe das Search verwendet, das standardmäßig verfügbar gemacht wird, anstatt die Rohkomponente zu verwenden.

Selbst wenn ich ihm setAlert und getRestaurants Requisiten gegeben habe, hat die Verbindungsmethode der Standardkomponente sie überschrieben und ihre eigenen setAlert und getRestaurants angegeben, deshalb waren sie es nie namens.

Die Rohkomponente ist nicht Redux-fähig, sondern holt nur Requisiten aus dem Redux-Store und verwendet sie. Da sich die Tests auf die Rohkomponente und nicht auf das Geschäft konzentrieren müssen, müssen wir sie für die Tests isoliert exportieren.

Ich benutze immer noch mockstore, wenn DisplaySearchBar gerendert wird.

Wie bereits in Search.js erwähnt, exportiere ich die Rohkomponente:

  // previous code
export const Search = ({ getRestaurants, setAlert }) => {
   // rest of the code

Und indem ich es anstelle der Standardkomponente teste, muss ich nur überprüfen, ob die setAlert und getRestaurants, die als Scheinfunktionen übergeben wurden, aufgerufen werden. (Test Nr. 3 und Nr. 4)

import React from 'react';
import { mount } from 'enzyme';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';

import { Search as BaseSearch } from './../Search';
import { DisplaySearchBar as BaseDisplaySearchBar } from '../../../layout/DisplaySearchBar/DisplaySearchBar';

const mockStore = configureStore([thunk]);
const initialState = {
  restaurants: { restaurants: ['foo'], alert: null },
};

const getRestaurants = jest.fn();
const setAlert = jest.fn();

let wrapper, store;

describe('Search', () => {
  beforeEach(() => {
    store = mockStore(initialState);

    wrapper = mount(
      <Provider store={store}>
        <BaseSearch setAlert={setAlert} getRestaurants={getRestaurants} />
      </Provider>
    );
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  test('1 - renders without errors', () => {
    expect(wrapper.find(BaseDisplaySearchBar)).toHaveLength(1);
  });

  test('2 - if restaurants clearButton is rendered', () => {
    expect(wrapper.find('[data-test="clear"]')).toBeTruthy();
  });

  test('3 - setAlert called if search button is pressed with no input', () => {
    wrapper.find('form').simulate('submit', { preventDefault: () => {} });

    expect(setAlert).toHaveBeenCalled();
  });

  test('4 - getRestaurants called when inputs filled and search button clicked ', () => {
    wrapper
      .find('[name="where"]')
      .at(0)
      .simulate('change', { target: { value: 'foo', name: 'where' } });

    wrapper
      .find('[name="what"]')
      .at(0)
      .simulate('change', { target: { value: 'foo', name: 'what' } });

    wrapper
      .find('[data-test="best_match"]')
      .at(0)
      .simulate('click');

    wrapper.find('form').simulate('submit', { preventDefault: () => {} });

    expect(getRestaurants).toHaveBeenCalled();
  });
});
0
Mugg84 22 Aug. 2020 im 08:15

Dies liegt daran, dass find() des Enzyms eine Sammlung von HTML-Knoten zurückgibt.

Erinnerst du dich an den Fehler dieses guten alten Enzyms?

Die Methode "simulieren" soll auf 1 Knoten ausgeführt werden.

Versuchen Sie es so: wrapper.find('...').at(0).

Wenn Sie Ihre verspotteten Ergebnisse von setAlert () and getRestaurant () to have been called, you refer to them in a way that unables us to know if it's a right or wrong reference. So, please supply your relevant debug () erwarten, verspotten Sie sie besser wie folgt:

const mockSetAlert = jest.fn();
const mockGetRestaurants = jest.fn();

const wrapper = mount(
    <Search setAlert={mockSetAlert} getRestaurants={mockGetRestaurants} />
);

...

expect(mockSetAlert).toHaveBeenCalled();
expect(mockGetRestaurants).toHaveBeenCalled();

Es ist ein vereinfachtes Beispiel, aber Sie bekommen die Idee ...

0
k-wasilewski 15 Aug. 2020 im 08:35

Search.js ist eine verbundene Komponente. Die Requisiten kommen über mapDispatchToProps aus dem Laden. Selbst wenn Sie die Requisiten verspotten, übernimmt der generierte Wrapper die entsprechenden Funktionen aus dem Store des Anbieters. Die Lösung besteht also darin, zu überprüfen, ob die Aktionen mit dem erforderlichen Typ und der erforderlichen Nutzlast aufgerufen wurden.

Ein weiteres Problem in Test 4 ist, dass Sie name nicht in event übergeben. Daher wurden die Werte im Zustand nicht festgelegt. Um solche Situationen zu vermeiden, verwenden Sie die Konsole, um Ihre Tests zu debuggen.

import React from 'react';
import { mount } from 'enzyme';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';

import Search from './../Search';
import DisplaySearchBar from '../../../layout/DisplaySearchBar/DisplaySearchBar';

import {
  SET_LOADING,
  SET_ALERT,

} from '../../../../actions/types';

const mockStore = configureStore([thunk]);
const initialState = {
  restaurants: { restaurants: ['foo'], alert: null },
};
const store = mockStore(initialState);
const mockSetAlert = jest.fn();
const mockGetRestaurants = jest.fn();

const wrapper = mount(
  <Provider store={store}>
    <Search setAlert={mockSetAlert} getRestaurants={mockGetRestaurants} />
  </Provider>
);

describe('Search', () => {
  afterEach(() => {
    jest.clearAllMocks();
  });

  test('1 - renders without errors', () => {
    expect(wrapper.find(DisplaySearchBar)).toHaveLength(1);
  });

  test('2 - if restaurants clearButton is rendered', () => {
    expect(wrapper.find('[data-test="clear"]')).toBeTruthy();
  });

  test('3 - setAlert called if search button is pressed with no input', () => {
    wrapper.find('form').simulate('submit', { preventDefault: () => {} });
    const actions= store.getActions();
    const expected={
      type: SET_ALERT,
      payload: expect.objectContaining({msg:"Please fill all the inputs"})
    };
    expect(actions[0]).toMatchObject(expected);
  });

  test('4 - getRestaurant called when inputs filled and search button clicked ', () => {
    wrapper
      .find('[name="where"]')
      .at(0)
      .simulate('change', { target: { value: 'foo', name:"where" } });

    wrapper
      .find('[name="what"]')
      .at(0)
      .simulate('change', { target: { value: 'foo',name:"what" } });

    wrapper
      .find('[data-test="best_match"]')
      .at(0)
      .simulate('click');

    wrapper.find('form').simulate('submit', { preventDefault: () => {} });
    const actions= store.getActions();
    const expected={
      type: SET_LOADING,
    };
    expect(actions).toContainEqual(expected);
     });
});
2
soumya sunny 17 Aug. 2020 im 17:23