在测试过程中模拟表单提交时,不会调用在Redux连接的组件中作为道具传递的动作

如何解决在测试过程中模拟表单提交时,不会调用在Redux连接的组件中作为道具传递的动作

我正在测试我的第一个应用程序,并且在测试Redux连接的组件时遇到了问题。

更具体地说,我正在测试Search.js。这个想法是在子组件DisplaySearcgBar.js中模拟表单提交,然后测试是否调用了setAlertgetRestaurants

在测试#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();

这是一个简化的示例,但您知道了...

,

我相信我会发现如何测试setAlertgetRestaurants是否被调用。 我使用的是默认公开的Search而不是原始组件。

因此,即使我给了它setAlertgetRestaurants道具,默认组件的connect方法也覆盖了它,并赋予了它自己的setAlertgetRestaurants,这就是为什么他们从未被召唤过。

原始组件不支持Redux,它只是从Redux商店获取道具并使用它们。由于测试需要专注于原始组件而不是商店,因此我们需要单独导出以进行测试。

在渲染mockstore时,我仍然使用DisplaySearchBar

正如我之前在Search.js中提到的,我导出了原始组件:

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

通过测试它而不是默认组件,我只需要检查是否调用了作为模拟功能传递的setAlertgetRestaurants。 (测试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 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-