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!!
3 Antworten
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();
});
});
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 ...
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);
});
});
Verwandte Fragen
Neue Fragen
reactjs
React ist eine JavaScript-Bibliothek zum Erstellen von Benutzeroberflächen. Es verwendet ein deklaratives, komponentenbasiertes Paradigma und soll sowohl effizient als auch flexibel sein.