如何为使用react-redux和react-router的Trade组件编写可被覆盖率统计的单元测试
Trade组件单元测试解决方案
问题原因
你当前的测试执行失败导致覆盖率未统计,根本原因是Trade组件依赖react-redux的useDispatch/useSelector、react-router-dom的useLocation三个钩子,直接渲染组件时缺少对应的上下文环境,测试抛出异常终止执行。
正确测试代码示例
我们采用钩子Mock的方式做单元测试,只验证Trade组件本身的逻辑,不耦合Redux、Router和子组件的实现:
import { render, screen } from '@testing-library/react'; import Trade from './trade'; import { useDispatch, useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import tradeActions from './trade-actions/trade-actions'; import appActions from '../app/actions/actions'; // Mock所有外部依赖钩子 jest.mock('react-redux', () => ({ useDispatch: jest.fn(), useSelector: jest.fn(), })); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: jest.fn(), })); // Mock子组件,避免子组件依赖影响测试 jest.mock('./TradeFilters/TradeFilters', () => () => <div data-testid="trade-filters" />); jest.mock('./TradeDataSeries/TradeDataSeries', () => ({ dataSource }) => <div data-testid="data-series" data-source={JSON.stringify(dataSource)} />); jest.mock('../../components/SelectedData/SelectedData', () => ({ facts }) => <div data-testid="selected-data" data-facts={JSON.stringify(facts)} />); // Mock action 避免真实副作用 jest.mock('./trade-actions/trade-actions', () => ({ fetchTradeDataseriesRequest: jest.fn(), fetchDatasourceDocumentsRequest: jest.fn(), fetchCountriesRequest: jest.fn(), fetchProductsRequest: jest.fn(), fetchBorderPointsRequest: jest.fn(), setSelectedDataSeries: jest.fn(), })); jest.mock('../app/actions/actions', () => ({ setDomain: jest.fn(), })); describe('Trade Component Test', () => { const mockDispatch = jest.fn(); const mockLocation = { pathname: '/trade' }; beforeEach(() => { // 每次用例执行前重置所有mock状态 jest.clearAllMocks(); useDispatch.mockReturnValue(mockDispatch); useLocation.mockReturnValue(mockLocation); }); it('基础渲染快照匹配', () => { // 给useSelector设置返回值 useSelector.mockImplementation((selector) => selector({ trade: { dataseries: [], facts: { total: 100, amount: 2000 } } })); const { asFragment } = render(<Trade />); expect(asFragment()).toMatchSnapshot(); // 验证子组件都渲染 expect(screen.getByTestId('trade-filters')).toBeInTheDocument(); expect(screen.getByTestId('data-series')).toBeInTheDocument(); expect(screen.getByTestId('selected-data')).toBeInTheDocument(); }); it('dataseries为空时挂载触发所有初始化请求', async () => { useSelector.mockImplementation((selector) => selector({ trade: { dataseries: [], facts: {} } })); render(<Trade />); // 验证初始化useEffect的dispatch expect(mockDispatch).toHaveBeenCalledWith(tradeActions.fetchTradeDataseriesRequest()); expect(mockDispatch).toHaveBeenCalledWith(tradeActions.fetchDatasourceDocumentsRequest()); expect(mockDispatch).toHaveBeenCalledWith(tradeActions.fetchCountriesRequest()); expect(mockDispatch).toHaveBeenCalledWith(tradeActions.fetchProductsRequest()); expect(mockDispatch).toHaveBeenCalledWith(tradeActions.fetchBorderPointsRequest()); expect(mockDispatch).toHaveBeenCalledWith(appActions.setDomain('trade')); // 验证第二个useEffect的dispatch expect(mockDispatch).toHaveBeenCalledWith(tradeActions.setSelectedDataSeries([])); expect(mockDispatch).toHaveBeenCalledTimes(7); }); it('dataseries不为空时挂载不重复触发数据请求', () => { useSelector.mockImplementation((selector) => selector({ trade: { dataseries: [{ id: 1, name: '测试数据' }], facts: {} } })); render(<Trade />); // 只会触发第二个useEffect的清空选择的请求 expect(mockDispatch).toHaveBeenCalledWith(tradeActions.setSelectedDataSeries([])); expect(mockDispatch).toHaveBeenCalledTimes(1); // 不会触发初始化数据请求 expect(tradeActions.fetchTradeDataseriesRequest).not.toHaveBeenCalled(); }); });
配套执行命令
你可以继续用原来的覆盖率命令验证:npm test -- --coverage --watchAll=false
同类组件测试通用思路
- 先Mock所有组件依赖的第三方钩子(redux、router、国际化等)
- Mock所有子组件,避免子组件的依赖影响当前组件测试
- 给钩子设置对应返回值模拟不同场景
- 验证渲染结果、副作用执行逻辑符合预期
内容的提问来源于stack exchange,提问作者joseluke
相关产品推荐
相关产品推荐

