react dispatch_React测试的那些事(三) React Hook 测试实例
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' }我們先寫個(gè)簡單的,只用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;這就是一個(gè)簡單的組件,通過dispatching 名為SUCCESS 的動作,把 stateprop1從 false 變成 true 。這是一個(gè)超基本的測試,保證initial state是我們想要的結(jié)果。
你可能想說,測試reducer就是測試實(shí)現(xiàn)的具體細(xì)節(jié),不建議這樣做的呀?但在實(shí)踐中發(fā)現(xiàn)這種測試還是很必要的,它也算作一種單元測試。
這個(gè)簡單的例子里面測試reducers看起來不是什么大事。當(dāng)狀態(tài)更復(fù)雜的情況不進(jìn)行測試會產(chǎn)生很多問題。所以請務(wù)必對actions和reducers進(jìn)行測試。
~useContext~
下面我們設(shè)想另一個(gè)場景,一個(gè)子組件能夠更新父組件的上下文環(huán)境的state。聽起來有點(diǎn)繞,實(shí)際上很簡單。
首先初始化一個(gè)Context對象
import React from 'react';const Context = React.createContext()export default Context父組件中提供Context.provider。傳遞給Provider的值是 App.js組件中setState函數(shù) 和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;子組件非常簡單:展示在父組件中初始化的文字,當(dāng)點(diǎn)擊按鈕時(shí)執(zhí)行setState函數(shù)。
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;父組件中狀態(tài)進(jìn)行了初始化和改變。我們只是用setState函數(shù)將狀態(tài)值傳遞給子組件。所以我們?nèi)缦逻M(jìn)行測試
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函數(shù)中寫了<Context.Provider/>和 <TestHookContext />,但實(shí)際上并沒必要。寫是為了容易理解代碼,不寫呢程序還是會運(yùn)行
const { container, getByText } = render(<App/>)~一點(diǎn)思考~
讓我們來回想下整個(gè)過程。所有的context state包含在父組件中,所以我們實(shí)際上測試的就是父組件,只是看起來像在用 useContext 測試著子組件而已。由于mount/render能渲染子組件(shallow不會渲染子組件),所以 <Context.Provider/> 和 <TestHookContext />這倆子組件被自動渲染出來了。
表單中的受控組件
受控組件代表著這個(gè)表單的state并沒有掌握在組件手里而在React的狀態(tài)中。每個(gè)按鍵都把輸入的內(nèi)容通過 onChange 保存在了React狀態(tài)里。
測試這樣的組件會比之前的復(fù)雜一些。
先看一個(gè)非常基本表單的組件
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()函數(shù)找到它。這也符合我們的測試原則,因?yàn)橛脩粼佥斎胫抵耙部吹膌abel呀。
- 我們用.change()代替了.click()事件,也可以用{target: {value: "Text"}}的方式傳遞假數(shù)據(jù)。
- 表單用event.target.value取值,這就是我們模擬事件時(shí)傳參的對象。
- 由于我們并不確定用戶輸入的是什么內(nèi)容,可以用.not確保渲染的內(nèi)容確實(shí)變了。
- 我們可以用相似方法測試表單的提交。不同之處為 .submit()傳這串信息{target: {text1: {value: 'Text'}}} (input元素的id是text1)
- 在這里用data-testid="form"匹配到我們的form元素,因?yàn)檫@是最優(yōu)的辦法了。
以上,介紹了獲取用戶提交表單的數(shù)據(jù)的方法。是不是和之前的例子相差不大?如果沒問題的話,接下來看點(diǎn)更復(fù)雜的吧。
useEffect 和 API請求
接下來我們看看如何測試useEffect hook 和 API請求(axios) ,與之前的都不太一樣。
先假設(shè)有一個(gè)url從 根組件傳遞到子組件
...<TestAxios url='https://jsonplaceholder.typicode.com/posts/1' />...簡單的發(fā)API請求并把結(jié)果保存在本地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;- 標(biāo)題的placeholder顯示的內(nèi)容是從一個(gè)三目運(yùn)算符中得來的。
- 本例仍需用 data-testid屬性 ,雖然用戶看不到也接觸不到它,但在API返回?cái)?shù)據(jù)之前不知道是什么值,所以靠此屬性來匹配到元素。
這里我們用mock數(shù)據(jù)(Mock是在測試中常用的模擬方法,比如用mock API 模擬真實(shí)的請求)因?yàn)橛谜鎸?shí)的數(shù)據(jù)進(jìn)行測試的話,拖慢了測試的速度,有時(shí)接口會有意外的錯(cuò)誤,測試數(shù)據(jù)會弄亂數(shù)據(jù)庫等問題。
~引入依賴~
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這個(gè)庫。
~mock~
是不是很奇怪,它怎么做到的?它用到了Jest提供的模擬功能。
首先我們創(chuàng)建一個(gè)__mocks__文件夾,位置與__test__相鄰。
在__mocks__文件夾中創(chuàng)建一個(gè) axios.js文件,它就是我們偽造的axios庫。在我們偽造的axios庫中加入jest mock 函數(shù)。嗯?這是什么函數(shù)?在jest環(huán)境中無需實(shí)現(xiàn)具體的請求邏輯,直接用這個(gè)模擬函數(shù)返回?cái)?shù)據(jù)即可。喏~ 看個(gè)例子
export default {get: jest.fn(() => Promise.resolve({ data: {} }) ) };- 此處簡單的示例中,偽造的get函數(shù)就是一個(gè)JS對象;
- get就是key值,value就是 mock 函數(shù)
- 就像一個(gè) axiosAPI請求,我們得到了一個(gè)promise
- 這個(gè)例子中沒有填寫任何返回?cái)?shù)據(jù),接下來我們會加上返回值
~加入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);})- 我們做的第一件事,調(diào)用了偽造的 axios get request ,偽造請求結(jié)果我們用的是jest提供的方法mockResolvedValue ,這個(gè)函數(shù)做的和它的函數(shù)名一樣,它像axios那樣 resolves一個(gè)promise 。
- mockResolvedValue需要在render之前進(jìn)行調(diào)用,否則test不會生效。因?yàn)樗俏覀儌卧斓?axios,當(dāng)執(zhí)行import axios from 'axios'; 時(shí),會導(dǎo)入我們偽造的axios,并把組件中用到的axios全部替換掉。
- 接下來,在promise返回前,一直處于加載狀態(tài),UI上出現(xiàn)...Loading。
- waitForElement()函數(shù)我們之前都沒見過,它會等到promise返回結(jié)果后才跳到下一個(gè)斷言。
- await 、 async 他們的用法與正常的非測試場景是一樣的。
- 當(dāng)解析出DOM后,UI會出現(xiàn)我們偽造的mock返回值“some title”
- 接下來我們要確保請求只調(diào)用了一次和url的正確性(雖然沒用到這個(gè)URL我們也要這么測試一下)
以上就是如何對axios的請求進(jìn)行測試,下面一章我們會講到如何用cypress進(jìn)行e to e測試。
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵(lì)來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的react dispatch_React测试的那些事(三) React Hook 测试实例的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 银行会破产吗?银行破产贷款还要还吗?
- 下一篇: 银行大额存单和大额存款区别是什么?