react dispatch_React测试的那些事(三) React Hook 测试实例

v2-888af5ed0ebd34d023ff4d36da55176a_1440w.jpg?source=172ae18b

useReducer

测试 useReducer 首先需要在组件中用 actions 和 reducers ,代码如下。

Reducer

import * as ACTIONS from './actions'

export const initialState = {
    stateprop1: false,
}

export const Reducer1 = (state = initialState, action) => {
  switch(action.type) {
    case "SUCCESS":
      return {
        ...state,
        stateprop1: true,
      }
    case "FAILURE":
      return {
        ...state,
        stateprop1: false,
      }
    default:
      return state
  }
}

actions

export const SUCCESS = {
  type: 'SUCCESS'
}

export const FAILURE = {
  type: 'FAILURE'
}

我们先写个简单的,只用action,不用action creators 代码如下:

import React, { useReducer } from 'react';
import * as ACTIONS from '../store/actions'
import * as Reducer from '../store/reducer'

const TestHookReducer = () => {
  const [reducerState, dispatch] = useReducer(Reducer.Reducer1, Reducer.initialState)

  const dispatchActionSuccess = () => {
    dispatch(ACTIONS.SUCCESS)
  }

  const dispatchActionFailure = () => {
    dispatch(ACTIONS.FAILURE)
  }

  return (
    <div>
       <div>
        {reducerState.stateprop1
           ? <p>stateprop1 is true</p>
           : <p>stateprop1 is false</p>}
       </div>
       <button onClick={dispatchActionSuccess}>
         Dispatch Success
       </button>
    </div>
  )
}

export default TestHookReducer;

这就是一个简单的组件,通过dispatching 名为SUCCESS 的动作,把 stateprop1从 false 变成 true 。这是一个超基本的测试,保证initial state是我们想要的结果。

你可能想说,测试reducer就是测试实现的具体细节,不建议这样做的呀?但在实践中发现这种测试还是很必要的,它也算作一种单元测试。

这个简单的例子里面测试reducers看起来不是什么大事。当状态更复杂的情况不进行测试会产生很多问题。所以请务必对actionsreducers进行测试。

~useContext~

下面我们设想另一个场景,一个子组件能够更新父组件的上下文环境的state。听起来有点绕,实际上很简单。

首先初始化一个Context对象

import React from 'react';

const Context = React.createContext()

export default Context

父组件中提供Context.provider。传递给Provider的值是 App.js组件中setState函数 和state

import React, { useState } from 'react';
import TestHookContext from './components/react-testing-lib/test_hook_context';


import Context from './components/store/context';


const App = () => {
  const [state, setState] = useState("Some Text")
  

  const changeText = () => {
    setState("Some Other Text")
  }


  return (
    <div className="App">
    <h1> Basic Hook useContext</h1>
     <Context.Provider value={{changeTextProp: changeText,
                               stateProp: state
                                 }} >
        <TestHookContext />
     </Context.Provider>
    </div>
  );
}

export default App;

子组件非常简单:展示在父组件中初始化的文字,当点击按钮时执行setState函数。

import React, { useContext } from 'react';

import Context from '../store/context';

const TestHookContext = () => {
  const context = useContext(Context)

  return (
    <div>
    <button onClick={context.changeTextProp}>
        Change Text
    </button>
      <p>{context.stateProp}</p>
    </div>
  )
}


export default TestHookContext;

父组件中状态进行了初始化和改变。我们只是用setState函数将状态值传递给子组件。所以我们如下进行测试

import React from 'react';
import ReactDOM from 'react-dom';
import TestHookContext from '../test_hook_context.js';
import {act, render, fireEvent, cleanup} from '@testing-library/react';
import App from '../../../App'

import Context from '../../store/context';

afterEach(cleanup)

it('Context value is updated by child component', () => {

   const { container, getByText } = render(<App>
                                            <Context.Provider>
                                             <TestHookContext />
                                            </Context.Provider>
                                           </App>);

   expect(getByText(/Some/i).textContent).toBe("Some Text")

   fireEvent.click(getByText("Change Text"))

   expect(getByText(/Some/i).textContent).toBe("Some Other Text")
})

虽然我们在render函数中写了<Context.Provider/><TestHookContext />,但实际上并没必要。写是为了容易理解代码,不写呢程序还是会运行

const { container, getByText } = render(<App/>) 

~一点思考~

让我们来回想下整个过程。所有的context state包含在父组件中,所以我们实际上测试的就是父组件,只是看起来像在用 useContext 测试着子组件而已。由于mount/render能渲染子组件(shallow不会渲染子组件),所以 <Context.Provider/><TestHookContext />这俩子组件被自动渲染出来了。

表单中的受控组件

受控组件代表着这个表单的state并没有掌握在组件手里而在React的状态中。每个按键都把输入的内容通过 onChange 保存在了React状态里。

测试这样的组件会比之前的复杂一些。

先看一个非常基本表单的组件

import React, { useState } from 'react';

const HooksForm1 = () => {
  const [valueChange, setValueChange] = useState('')
  const [valueSubmit, setValueSubmit] = useState('')

  const handleChange = (event) => (
    setValueChange(event.target.value)
  );

  const handleSubmit = (event) => {
    event.preventDefault();
    setValueSubmit(event.target.text1.value)
  };

    return (
      <div>
       <h1> React Hooks Form </h1>
        <form data-testid="form" onSubmit={handleSubmit}>
          <label htmlFor="text1">Input Text:</label>
          <input id="text1" onChange={handleChange} type="text" />
          <button type="submit">Submit</button>
        </form>
        <h3>React State:</h3>
          <p>Change: {valueChange}</p>
          <p>Submit Value: {valueSubmit}</p>
        <br />
      </div>
    )
}

export default HooksForm1;

组件很简单,包含form中基本的change、submit操作,form的data-testid=form可以作为查询的ID值。

测试

import React from 'react';
import ReactDOM from 'react-dom';
import HooksForm1 from '../test_hook_form.js';
import {render, fireEvent, cleanup} from '@testing-library/react';

afterEach(cleanup)

//testing a controlled component form.
it('Inputing text updates the state', () => {
    const { getByText, getByLabelText } = render(<HooksForm1 />);

    expect(getByText(/Change/i).textContent).toBe("Change: ")

    fireEvent.change(getByLabelText("Input Text:"), {target: {value: 'Text' } } )

    expect(getByText(/Change/i).textContent).not.toBe("Change: ")
 })


 it('submiting a form works correctly', () => {
     const { getByTestId, getByText } = render(<HooksForm1 />);

     expect(getByText(/Submit Value/i).textContent).toBe("Submit Value: ")

     fireEvent.submit(getByTestId("form"), {target: {text1: {value: 'Text' } } })

     expect(getByText(/Submit Value/i).textContent).not.toBe("Submit Value: ")
  })
  • 由于input元素还没有输入值,我们用getByLabelText()函数找到它。这也符合我们的测试原则,因为用户再输入值之前也看的label呀。
  • 我们用.change()代替了.click()事件,也可以用{target: {value: "Text"}}的方式传递假数据。
  • 表单用event.target.value取值,这就是我们模拟事件时传参的对象。
  • 由于我们并不确定用户输入的是什么内容,可以用.not确保渲染的内容确实变了。
  • 我们可以用相似方法测试表单的提交。不同之处为 .submit()传这串信息{target: {text1: {value: 'Text'}}} (input元素的id是text1)
  • 在这里用data-testid="form"匹配到我们的form元素,因为这是最优的办法了。

以上,介绍了获取用户提交表单的数据的方法。是不是和之前的例子相差不大?如果没问题的话,接下来看点更复杂的吧。

useEffect 和 API请求

接下来我们看看如何测试useEffect hook 和 API请求(axios) ,与之前的都不太一样。

先假设有一个url从 根组件传递到子组件

...

     <TestAxios url='https://jsonplaceholder.typicode.com/posts/1' />
     
 ... 

简单的发API请求并把结果保存在本地state的组件

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const TestAxios = (props) => {
  const [state, setState] = useState()

  useEffect(() => {
    axios.get(props.url)
      .then(res => setState(res.data))
  }, [])

  return (
    <div>
    <h1> Axios Test </h1>
        {state
          ? <p data-testid="title">{state.title}</p>
          : <p>...Loading</p>}
    </div>
  )
}

export default TestAxios;
  • 标题的placeholder显示的内容是从一个三目运算符中得来的。
  • 本例仍需用 data-testid属性 ,虽然用户看不到也接触不到它,但在API返回数据之前不知道是什么值,所以靠此属性来匹配到元素。

这里我们用mock数据(Mock是在测试中常用的模拟方法,比如用mock API 模拟真实的请求)因为用真实的数据进行测试的话,拖慢了测试的速度,有时接口会有意外的错误,测试数据会弄乱数据库等问题。

~引入依赖~

import React from 'react';
import ReactDOM from 'react-dom';
import TestAxios from '../test_axios.js';
import {act, render, fireEvent, cleanup, waitForElement} from '@testing-library/react';

import axiosMock from "axios";

有句之前没介绍过的引入 import axiosMock from "axios";它不是说从axios库中引入axiosMock,而是mock了axios这个库。

~mock~

是不是很奇怪,它怎么做到的?它用到了Jest提供的模拟功能。

首先我们创建一个__mocks__文件夹,位置与__test__相邻。

__mocks__文件夹中创建一个 axios.js文件,它就是我们伪造的axios库。在我们伪造的axios库中加入jest mock 函数。嗯?这是什么函数?在jest环境中无需实现具体的请求逻辑,直接用这个模拟函数返回数据即可。喏~ 看个例子

export default {
  get: jest.fn(() => Promise.resolve({ data: {} }) )
};
  • 此处简单的示例中,伪造的get函数就是一个JS对象;
  • get就是key值,value就是 mock 函数
  • 就像一个 axiosAPI请求,我们得到了一个promise
  • 这个例子中没有填写任何返回数据,接下来我们会加上返回值

~加入mock返回值的测试~

//imports
...

afterEach(cleanup)

it('Async axios request works', async () => {
  axiosMock.get.mockResolvedValue({data: { title: 'some title' } })

  const url = 'https://jsonplaceholder.typicode.com/posts/1'
  const { getByText, getByTestId, rerender } = render(<TestAxios url={url} />);

  expect(getByText(/...Loading/i).textContent).toBe("...Loading")

  const resolvedEl = await waitForElement(() => getByTestId("title"));

  expect((resolvedEl).textContent).toBe("some title")

  expect(axiosMock.get).toHaveBeenCalledTimes(1);
  expect(axiosMock.get).toHaveBeenCalledWith(url);
 })
  • 我们做的第一件事,调用了伪造的 axios get request ,伪造请求结果我们用的是jest提供的方法mockResolvedValue ,这个函数做的和它的函数名一样,它像axios那样 resolves一个promise
  • mockResolvedValue需要在render之前进行调用,否则test不会生效。因为它是我们伪造的 axios,当执行import axios from 'axios'; 时,会导入我们伪造的axios,并把组件中用到的axios全部替换掉。
  • 接下来,在promise返回前,一直处于加载状态,UI上出现...Loading
  • waitForElement()函数我们之前都没见过,它会等到promise返回结果后才跳到下一个断言。
  • awaitasync 他们的用法与正常的非测试场景是一样的。
  • 当解析出DOM后,UI会出现我们伪造的mock返回值“some title”
  • 接下来我们要确保请求只调用了一次和url的正确性(虽然没用到这个URL我们也要这么测试一下)

以上就是如何对axios的请求进行测试,下面一章我们会讲到如何用cypress进行e to e测试。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: React中的dispatch是指Redux中的一个函数,它用于触发Redux中的action,从而更新state。 在React中,dispatch通常是通过connect函数和mapDispatchToProps函数与组件进行绑定。mapDispatchToProps函数会将dispatch函数映射到组件的props上,这样组件就可以通过调用props上的dispatch来触发action,从而更新state。 例如,假设我们有一个action叫做increment,用于增加计数器的值。我们可以定义一个mapDispatchToProps函数,将increment action映射到props上: ``` import { increment } from './actions'; const mapDispatchToProps = dispatch => { return { increment: () => dispatch(increment()) } } ``` 然后在组件中就可以通过props调用dispatch来触发increment action: ``` import React from 'react'; import { connect } from 'react-redux'; const Counter = ({ count, increment }) => { return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); }; const mapStateToProps = state => { return { count: state.count }; }; export default connect(mapStateToProps, mapDispatchToProps)(Counter); ``` 这样,每当用户点击“Increment”按钮时,就会触发increment action,从而更新state中的计数器的值。 ### 回答2: React dispatchReact 中用于触发和传递数据的机制。它是 Redux 中常用的一个方法,用于派发(dispatch)一个 action 到 store。当我们在组件中需要触发一个 action 来更新 state 时,可以使用 dispatch 方法。 通过 dispatch,我们可以将一个 action 传递给 Redux 的 store,然后 store 会根据 action 的类型来更新对应的 state。它类似于使用件来触发状态的改变。当我们在组件中调用 dispatch 方法时,Redux 会根据传入的 action 的 type 属性,来决定如何更新 state。 dispatch 方法接收一个 action 对象作为参数,这个对象必须要有一个 type 属性来指定该 action 的类型。我们可以根据业务需要,自定义不同的 action 类型来触发不同的状态更新操作。dispatch 方法会将这个 action 对象传递给 store,并将其作为参数传递给 reducer 函数进行处理。 在 Redux 应用中,dispatch 是一个非常重要的方法,它实现了应用的状态控制。我们可以在组件中通过 mapDispatchToProps 方法将 dispatch 绑定到组件的 props 上,方便在组件中直接调用 dispatch 方法。 总结来说,React dispatchReact 提供的一种数据触发和传递的机制,可以通过 dispatch 方法将一个 action 发送给 Redux 的 store,然后根据 action 的类型来更新对应的 state。它是实现状态管理的重要方法之一,能够帮助我们更好地控制应用的状态变化。 ### 回答3: React中的dispatch是一种用于发送操作或件的方法。它被用于与Redux或其他状态管理库一起使用,用于更新应用程序的状态并处理数据流。 当我们在React中使用Redux时,我们通过dispatch来发送action,即一个用于描述发生的件的对象。dispatch将action作为参数,并将其发送到Redux store中。Redux store会将action传递给reducer函数进行处理,这个函数会根据action的类型来更新store中的状态。 使用dispatch,我们可以在应用程序中的任何地方发送action,并使用reducer来处理这些action,并相应地更新store的状态。这种方式使我们能够实现单向数据流和可预测的状态管理。 除了Redux,我们还可以使用React的内置useReducer hook来实现类似的功能。useReducer接受一个reducer函数和初始状态作为参数,并返回当前状态和一个dispatch函数。使用dispatch函数,我们可以在组件中发送action,并在reducer函数中根据action类型来更新状态。 总而言之,dispatchReact中用于发送操作或件的方法。它与Redux或其他状态管理库一起使用,用于更新应用程序的状态并处理数据流。无论我们是使用Redux还是React的useReducer,dispatch都是实现状态管理的关键。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值