【发布时间】:2020-08-22 16:15:55
【问题描述】:
我正在测试我的第一个应用,但在测试 Redux 连接组件时遇到了问题。
更具体地说,我正在测试Search.js。这个想法是在子组件DisplaySearcgBar.js中模拟一个表单提交,然后测试是否调用了setAlert和getRestaurants。
在测试 #3 中,因为在提交表单时输入为空,Search.js 应该调用 OnSubmit(),而在 #4 中它应该调用 getRestaurants,因为提供了输入。
两个测试都因相同的错误而被拒绝:
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)
我是测试新手,我不确定自己做错了什么。
我尝试了不同的方法来选择这两个函数,但要么我得到了上面相同的错误,要么找不到它们。 我觉得我在兜圈子,我一定是错过了什么,但我不明白是什么。
这里是 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',
});
});
});
搜索.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);
按钮所在的子组件
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);
};
这里是 Github 上的完整存储库:https://github.com/mugg84/RestaurantFinderRedux.git
提前感谢您的帮助!!
【问题讨论】:
标签: reactjs unit-testing react-redux jestjs enzyme