如何测试组件中的react useContext useReducer调度
Posted
技术标签:
【中文标题】如何测试组件中的react useContext useReducer调度【英文标题】:How to test react useContext useReducer dispatch in component 【发布时间】:2020-11-18 09:04:10 【问题描述】:希望有人能给我指出正确的方向。基本上我已经创建了一个使用钩子的反应应用程序,特别是 useContext、useEffect 和 useReducer。我的问题是我似乎无法通过测试来检测相关组件的点击或调度事件。
可以在以下位置找到我的应用程序的精简版:https://github.com/atlantisstorm/hooks-testing 测试与 layout.test.js 脚本相关。
我尝试了各种方法、模拟调度、useContext 等的不同方式,但对它并不满意。最新版本。
layout.test.js
import React from 'react';
import render, fireEvent from "@testing-library/react";
import Layout from './layout';
import App from './app';
import Provider, initialState from './context';
const dispatch = jest.fn();
const spy = jest
.spyOn(React, 'useContext')
.mockImplementation(() => (
state: initialState,
dispatch: dispatch
));
describe('Layout component', () =>
it('starts with a count of 0', () =>
const getByTestId = render(
<App>
<Provider>
<Layout />
</Provider>
</App>
);
expect(dispatch).toHaveBeenCalledTimes(1);
const refreshButton = getByTestId('fetch-button');
fireEvent.click(refreshButton);
expect(dispatch).toHaveBeenCalledTimes(3);
);
);
layout.jsx
import React, useContext, useEffect from 'react';
import Context from "./context";
const Layout = () =>
const state, dispatch = useContext(Context);
const displayThings, things = state;
const onClickDisplay = (event) =>
// eslint-disable-next-line
event.preventDefault;
dispatch( type: "DISPLAY_THINGS" );
;
useEffect(() =>
dispatch( type: "FETCH_THINGS" );
, [displayThings]);
const btnText = displayThings ? "hide things" : "display things";
return (
<div>
<button data-testid="fetch-button" onClick=onClickDisplay>btnText</button>
displayThings ?
<p>We got some things!</p>
:
<p>No things to show!</p>
displayThings && things.map((thing) =>
<p> thing </p>
)
</div>
)
export default Layout;
app.jsx
import React from 'react';
import Provider from "./context";
import Layout from './layout';
const App = () =>
return (
<Provider>
<Layout />
</Provider>
)
export default App;
context.jsx
import React, createContext, useReducer from "react";
import reducer from "./reducer";
export const Context = createContext();
export const initialState =
displayThings: false,
things: []
;
export const Provider = ( children ) =>
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Context.Provider value= state, dispatch >
children
</Context.Provider>
);
;
export default Provider;
reducer.jsx
export const reducer = (state, action) =>
switch (action.type)
case "DISPLAY_THINGS":
const displayThings = state.displayThings ? false : true;
return ...state, displayThings ;
case "FETCH_THINGS":
const things = state.displayThings ? [
"thing one",
"thing two"
] : [];
return ...state, things ;
default:
return state;
;
我确信当我看到它时答案会很容易,但只是想弄清楚我可以检测到点击事件并检测到“调度”事件吗? (我已经在主应用程序中进行了单独的测试,以正确测试调度响应/操作)
提前谢谢你。
编辑 好的,我想我有一个合理但不完美的解决方案。首先,我只是在 context.jsx 模块中添加了可选的 testDispatch 和 testState 属性。
新的 context.jsx
import React, createContext, useReducer from "react";
import reducer from "./reducer";
export const Context = createContext();
export const initialState =
displayThings: false,
things: []
;
export const Provider = ( children, testDispatch, testState ) =>
const [iState, iDispatch] = useReducer(reducer, initialState);
const dispatch = testDispatch ? testDispatch : iDispatch;
const state = testState ? testState : iState;
return (
<Context.Provider value= state, dispatch >
children
</Context.Provider>
);
;
export default Provider;
然后在 layout.test.jsx 中,我只是简单地传入模拟的 jest 调度函数和必要的状态。还移除了外部 App 包装,因为这似乎可以防止道具通过。
新的 layout.test.jsx
import React from 'react';
import render, fireEvent from "@testing-library/react";
import Layout from './layout';
import Provider from './context';
describe('Layout component', () =>
it('starts with a count of 0', () =>
const dispatch = jest.fn();
const state =
displayThings: false,
things: []
;
const getByTestId = render(
<Provider testDispatch=dispatch testState=state>
<Layout />
</Provider>
);
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenNthCalledWith(1, type: "FETCH_THINGS" );
const refreshButton = getByTestId('fetch-things-button');
fireEvent.click(refreshButton);
expect(dispatch).toHaveBeenCalledTimes(2);
// Important: The order of the calls should be this, but dispatch is reporting them
// the opposite way around in the this test, i.e. FETCH_THINGS, then DISPLAY_THINGS...
//expect(dispatch).toHaveBeenNthCalledWith(1, type: "DISPLAY_THINGS" );
//expect(dispatch).toHaveBeenNthCalledWith(2, type: "FETCH_THINGS" );
// ... so as dispatch behaves correctly outside of testing for the moment I'm just settling for
// knowing that dispatch was at least called twice with the correct parameters.
expect(dispatch).toHaveBeenCalledWith( type: "DISPLAY_THINGS" );
expect(dispatch).toHaveBeenCalledWith( type: "FETCH_THINGS" );
);
);
不过,如上所述,有一点需要注意,当“fetch-things-button”被触发时,它会以错误的顺序报告调度。 :/ 所以我只是决定知道触发的正确调用,但是如果有人知道为什么调用顺序不符合预期,我会很高兴知道。
https://github.com/atlantisstorm/hooks-testing 如果有人感兴趣,请更新以反映上述内容。
【问题讨论】:
这个问题有更新吗? 您应该开始测试 UI 交互和结果,而不是实现本身。这是使用 react-testing-library 的正确方法 【参考方案1】:几个月前,我还尝试为应用程序的 reducer + 上下文编写单元测试。所以,这是我测试 useReducer 和 useContext 的解决方案。
FeaturesProvider.js
import React, createContext, useContext, useReducer from 'react';
import featuresInitialState, featuresReducer from '../reducers/featuresReducer';
export const FeatureContext = createContext();
const FeaturesProvider = ( children ) =>
const [state, dispatch] = useReducer(featuresReducer, featuresInitialState);
return <FeatureContext.Provider value= state, dispatch >children</FeatureContext.Provider>;
;
export const useFeature = () => useContext(FeatureContext);
export default FeaturesProvider;
FeaturesProvider.test.js
import React from 'react';
import render from '@testing-library/react';
import renderHook from '@testing-library/react-hooks';
import FeaturesProvider, useFeature, FeatureContext from './FeaturesProvider';
const state = features: [] ;
const dispatch = jest.fn();
const wrapper = ( children ) => (
<FeatureContext.Provider value= state, dispatch >
children
</FeatureContext.Provider>
);
const mockUseContext = jest.fn().mockImplementation(() => ( state, dispatch ));
React.useContext = mockUseContext;
describe('useFeature test', () =>
test('should return present feature toggles with its state and dispatch function', () =>
render(<FeaturesProvider />);
const result = renderHook(() => useFeature(), wrapper );
expect(result.current.state.features.length).toBe(0);
expect(result.current).toEqual( state, dispatch );
);
);
featuresReducer.js
import ApplicationConfig from '../config/app-config';
import actions from './actions';
export const featuresInitialState =
features: [],
environments: ApplicationConfig.ENVIRONMENTS,
toastMessage: null
;
const INITIALIZE_DATA, TOGGLE_FEATURE, ENABLE_OR_DISABLE_TOAST = actions;
export const featuresReducer = (state, type, payload ) =>
switch (type)
case INITIALIZE_DATA:
return
...state,
[payload.name]: payload.data
;
case TOGGLE_FEATURE:
return
...state,
features: state.features.map((feature) => (feature.featureToggleName === payload.featureToggleName
?
...feature,
environmentState:
...feature.environmentState, [payload.environment]: !feature.environmentState[payload.environment]
: feature))
;
case ENABLE_OR_DISABLE_TOAST:
return ...state, toastMessage: payload.message ;
default:
return ...state ;
;
featuresReducer.test.js
import featuresReducer from './featuresReducer';
import actions from './actions';
const INITIALIZE_DATA, TOGGLE_FEATURE, ENABLE_OR_DISABLE_TOAST = actions;
describe('Reducer function test', () =>
test('should initialize data when INITIALIZE_DATA action is dispatched', () =>
const featuresState =
features: []
;
const action =
type: INITIALIZE_DATA,
payload:
name: 'features',
data: [
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: sit: true, replica: true, prod: false
]
;
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual(
features: [
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: sit: true, replica: true, prod: false
]
);
);
test('should toggle the feature for the given feature and environemt when TOGGLE_FEATURE action is disptched', () =>
const featuresState =
features: [
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: sit: true, replica: true, prod: false
,
featureId: '23458', featureName: 'WO photo download', featureToggleName: '23458_WOPhotoDownload', environmentState: sit: true, replica: true, prod: false
]
;
const action =
type: TOGGLE_FEATURE,
payload: featureToggleName: '23456_WOPhotoDownload', environment: 'sit'
;
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual(
features: [
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: sit: false, replica: true, prod: false
,
featureId: '23458', featureName: 'WO photo download', featureToggleName: '23458_WOPhotoDownload', environmentState: sit: true, replica: true, prod: false
]
);
);
test('should enable the toast message when ENABLE_OR_DISABLE_TOAST action is dispatched with the message as part of payload', () =>
const featuresState =
toastMessage: null
;
const action =
type: ENABLE_OR_DISABLE_TOAST,
payload: message: 'Something went wrong!'
;
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual( toastMessage: 'Something went wrong!' );
);
test('should disable the toast message when ENABLE_OR_DISABLE_TOAST action is dispatched with message as null as part of payload', () =>
const featuresState =
toastMessage: null
;
const action =
type: ENABLE_OR_DISABLE_TOAST,
payload: message: null
;
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual( toastMessage: null );
);
test('should return the current state when the action with no specific type is dispatched', () =>
const featuresState =
features: [
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: sit: false, replica: true, prod: false
]
;
const action = ;
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual(
features: [
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: sit: false, replica: true, prod: false
]
);
);
);
【讨论】:
以上是关于如何测试组件中的react useContext useReducer调度的主要内容,如果未能解决你的问题,请参考以下文章
如何在类组件中使用 React.useContext(AuthContext) 来触发 index.js 文件中的条件集
如何将 React 钩子(useContext、useEffect)与 Apollo 反应钩子(useQuery)结合起来