如何使用JEST对使用Redux及Hooks的React函数组件进行单元测试
测试代码问题排查与修改方案
核心问题梳理
- 错误mock了
useEffect:你直接将useEffectmock为空实现,导致组件内触发dispatch(getDetails)的逻辑完全不会执行 useSelectormock逻辑错误:多次调用mockReturnValue会覆盖前一次的返回值,且没有按照selector入参正确返回state对应字段,还存在拼写错误mocdata- 错误使用
renderHook测试组件:renderHook是用于测试自定义Hook的API,测试React组件应该使用@testing-library/react的render方法 useDispatchmock逻辑错误:你直接返回了getDetails的调用结果,而useDispatch应该返回一个可调用的dispatch函数- 无效的
sleep调用:Jest测试中普通sleep无法等待异步逻辑执行,当前场景也不需要延迟等待 - redux-mock-store只记录action不会更新state:如果要测试不同accounts值下的渲染效果,直接修改mock的state值即可,不需要模拟dispatch更新流程
修改后完整测试代码
import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import * as reactRedux from 'react-redux'; import * as SelectionActions from './你的action文件路径'; import MyFunctionalComponent from './你的组件路径'; // 统一mock react-redux的hook jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), useSelector: jest.fn(), useDispatch: jest.fn() })); const mockUser = { id: '32sdfsdfr-fjdfdk33-4434sdfs', otherid: '73587hdfjg-dfghd94-jgdj' }; describe('<MyFunctionalComponent/>', () => { const useSelectorMock = reactRedux.useSelector; const useDispatchMock = reactRedux.useDispatch; const mockDispatch = jest.fn(); beforeEach(() => { useDispatchMock.mockReturnValue(mockDispatch); // 每次测试前重置所有mock jest.clearAllMocks(); }); // 测试用例1:初始加载accounts为null,触发getDetails调用 it('should dispatch getDetails when accounts is null', () => { // 按组件内useSelector的调用逻辑返回对应state useSelectorMock.mockImplementation(selector => selector({ user: mockUser, Selection: { accounts: null } }) ); const getDetailsSpy = jest.spyOn(SelectionActions, 'getDetails').mockReturnValue({ type: 'GET_DETAILS' }); render(<MyFunctionalComponent />); expect(getDetailsSpy).toHaveBeenCalledWith(mockUser.id, mockUser.otherid); expect(mockDispatch).toHaveBeenCalledWith(getDetailsSpy.mock.results[0].value); expect(screen.getByTestId('circular-loader')).toBeInTheDocument(); // 给CircularLoader加data-testid属性方便验证 }); // 测试用例2:accounts长度大于1,渲染MyList it('should render MyList when there are more than 1 accounts', () => { const mockAccounts = [ { id: '637ghgh' }, { id: '10a190abd' } ]; useSelectorMock.mockImplementation(selector => selector({ user: mockUser, Selection: { accounts: mockAccounts } }) ); const { asFragment } = render(<MyFunctionalComponent />); expect(screen.getByTestId('my-list')).toBeInTheDocument(); // 给MyList加data-testid属性 expect(asFragment()).toMatchSnapshot(); }); // 测试用例3:accounts为空数组,渲染AccessUnavailablePanel it('should render AccessUnavailablePanel when accounts is empty array', () => { useSelectorMock.mockImplementation(selector => selector({ user: mockUser, Selection: { accounts: [] } }) ); const { asFragment } = render(<MyFunctionalComponent />); expect(screen.getByTestId('access-unavailable')).toBeInTheDocument(); // 给AccessUnavailablePanel加data-testid属性 expect(asFragment()).toMatchSnapshot(); }); });
补充说明
- 如果需要测试异步action的执行逻辑,可以单独对
getDetails这个action creator写单元测试,不需要和组件渲染逻辑耦合在一起 - 给组件加对应的
data-testid属性可以更稳定的定位元素,避免文本或者样式修改导致测试失败 - 如果要测试点击事件的dispatch逻辑,可以模拟点击MyList的项,验证
saveId对应的action被正确dispatch即可
内容的提问来源于stack exchange,提问作者roma
相关产品推荐
相关产品推荐

