如何解决在测试过程中模拟表单提交时,不会调用在Redux连接的组件中作为道具传递的动作
我正在测试我的第一个应用程序,并且在测试Redux连接的组件时遇到了问题。
更具体地说,我正在测试Search.js
。这个想法是在子组件DisplaySearcgBar.js
中模拟表单提交,然后测试是否调用了setAlert
和getRestaurants
。
在测试#3中,由于提交表单时输入为空,Search.js
应该调用OnSubmit()
,后者应该调用setAlert
,而在#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',});
});
});
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);
按钮所在的子组件
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,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
预先感谢您的帮助!
解决方法
Search.js是一个连接的组件。它的道具通过mapDispatchToProps来自商店。即使您模拟道具,生成的包装器也会从提供者的商店中获取相应的功能。因此,解决方案是检查是否已使用所需的类型和有效负载调用了动作。
test-4中的另一个问题是您没有在name
内传递event
。因此,未在状态中设置值。为了避免这种情况,请使用控制台调试测试。
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',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);
});
});
,
那是因为酶的find()
返回了html节点的集合。
还记得这个好酶吗?
“模拟”方法应在1个节点上运行。
尝试如下:wrapper.find('...').at(0)
。
此外,当您期望模拟的‘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()`结果时,或者更好的是,像这样模拟它们:
const mockSetAlert = jest.fn();
const mockGetRestaurants = jest.fn();
const wrapper = mount(
<Search setAlert={mockSetAlert} getRestaurants={mockGetRestaurants} />
);
...
expect(mockSetAlert).toHaveBeenCalled();
expect(mockGetRestaurants).toHaveBeenCalled();
这是一个简化的示例,但您知道了...
,我相信我会发现如何测试setAlert
和getRestaurants
是否被调用。
我使用的是默认公开的Search
而不是原始组件。
因此,即使我给了它setAlert
和getRestaurants
道具,默认组件的connect方法也覆盖了它,并赋予了它自己的setAlert
和getRestaurants
,这就是为什么他们从未被召唤过。
原始组件不支持Redux,它只是从Redux商店获取道具并使用它们。由于测试需要专注于原始组件而不是商店,因此我们需要单独导出以进行测试。
在渲染mockstore
时,我仍然使用DisplaySearchBar
。
正如我之前在Search.js
中提到的,我导出了原始组件:
// previous code
export const Search = ({ getRestaurants,setAlert }) => {
// rest of the code
通过测试它而不是默认组件,我只需要检查是否调用了作为模拟功能传递的setAlert
和getRestaurants
。 (测试3和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'],};
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',{ preventDefault: () => {} });
expect(setAlert).toHaveBeenCalled();
});
test('4 - getRestaurants called when inputs filled and search button clicked ',name: 'where' } });
wrapper
.find('[name="what"]')
.at(0)
.simulate('change',name: 'what' } });
wrapper
.find('[data-test="best_match"]')
.at(0)
.simulate('click');
wrapper.find('form').simulate('submit',{ preventDefault: () => {} });
expect(getRestaurants).toHaveBeenCalled();
});
});
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。