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
看起来不是什么大事。当状态更复杂的情况不进行测试会产生很多问题。所以请务必对actions
和reducers
进行测试。
~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 函数
- 就像一个
axios
API请求,我们得到了一个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
返回结果后才跳到下一个断言。await
、async
他们的用法与正常的非测试场景是一样的。- 当解析出DOM后,UI会出现我们伪造的mock返回值“some title”
- 接下来我们要确保请求只调用了一次和url的正确性(虽然没用到这个URL我们也要这么测试一下)
以上就是如何对axios的请求进行测试,下面一章我们会讲到如何用cypress
进行e to e
测试。