【问题标题】:How to unit test a React functional component using hooks useEffect,useDispatch and useSelector and Redux?如何使用钩子 useEffect、useDispatch 和 useSelector 以及 Redux 对 React 功能组件进行单元测试?
【发布时间】:2021-10-30 22:39:34
【问题描述】:

我正在尝试测试以下功能组件:

export const MyFunctionalComponent=()=>{ 

const {id, otherid} = useSelector((state) => state.user);
    const accounts = useSelector((state) => state.Selection.accounts);

    const dispatch = useDispatch();

    useEffect(() => {
        if (!accounts) {
            dispatch(getDetails(id, otherid));
        }
    }, []);

    const handleOnClick = (Id) => {
        dispatch(saveId(Id));
    };

    if (!accounts) {
        return <CircularLoader />;
    }

    if (!accounts.length) {
        return (
            <AccessUnavailablePanel
               
            />
        );
    }

    if (accounts.length === 1) {
        return <Redirect to={`${accounts[0].id}`} />;
    }

    return (
        <MyList
            accounts={accounts}
            handleOnClick={handleOnClick}
        />
    );
};

我是 JEST 新手,如何正确模拟 useSelector、useDispatch 和 useEffect?

我正在尝试如下测试:


jest.mock('react-redux', () => ({
    ...jest.requireActual('react-redux'),
    useSelector: jest.fn(),
    useDispatch: jest.fn()
}));

describe('<MyFunctionalComponent/>', () => {
    const useEffect=jest.spyOn(React, "useEffect").mockImplementation(() => {})
    const useSelectorMock = reactRedux.useSelector;
    const useDispatchMock = reactRedux.useDispatch;
    const user = {
        id: '32sdfsdfr-fjdfdk33-4434sdfs',
        otherid: '73587hdfjg-dfghd94-jgdj'
    };
    const store = mockStore({
        user:user,
        Selection: {
            Id: '',
            accounts: null
        }
    });

    let wrapper;
    const setup = () => {
        const {result} = renderHook(() => MyFunctionalComponent(), {
            wrapper: ({children}) => (
                <Provider store={store}>{children}</Provider>
            )
        });
        return result;
    };

    describe('Rendering accounts available', () => {
        beforeEach(() => {
            useDispatchMock.mockImplementation(() => () => {});
            useSelectorMock.mockImplementation((selector) =>
                selector(mockStore)
            );
        });
        afterEach(() => {
            jest.clearAllMocks();
            useSelectorMock.mockClear();
            useDispatchMock.mockClear();
        });
        it('should render the accounts if more than 1 account available', () => {
            useSelectorMock.mockReturnValue(user);
            const mockdata = {
                accounts: [
                    {
                       id: '637ghgh',
                       
                    },
                    {
                        id: '10a190abd',
                       
                    }
                ]
            };
            const getDetails = jest.spyOn(
                SelectionActions,
                'getDetails'
            );
            useSelectorMock.mockReturnValue(mocdata);
            useDispatchMock.mockReturnValue(
                getDetails(user.id, user.otherid)
            );
            wrapper = setup();
            store.dispatch(getDetails(user.id, user.otherid))
            sleep(500);
            const actions=store.getActions();
            console.debug(actions[0])
            expect(getDetails).toHaveBeenCalledWith(user.id, user.otherid);
            expect(actions[0].type).toEqual('GET_DETAILS')
            expect(wrapper.current).toMatchSnapshot();
        });
});

但我的快照似乎返回 AccessUnavialable,并且有效负载未定义。如何修改我的测试以正确地将模拟数据作为有效负载返回并获取正确的快照?

【问题讨论】:

  • 我不会模拟useEffect 也不会模拟useDispatch,因为您已经将组件包装在Provider 中。另外,mockdata 不应该是一个数组吗?
  • @alextrastero 将模拟定义为对象是愚蠢的。当我将其更改为数组时它起作用了。谢谢。

标签: reactjs unit-testing react-redux jestjs react-hooks


【解决方案1】:

可以通过react-redux等mock库来测试,但不推荐。它过于关注实现细节。更好的方法是使用模拟存储来提供模拟数据而不是模拟实现细节。使用原始的、未模拟的useSelectoruseEffect 钩子,它们更接近代码的真实功能,而不是模拟的实现细节。

此外,创建模拟对象及其实现也是非常繁琐的步骤。还应注意清理 mock 对象,以免被其他测试用例使用,导致测试失败。

使用redux-mock-store 创建一个具有模拟状态的模拟商店。

每个测试用例提供不同的mock数据来驱动组件渲染,最后断言组件渲染是否正确。

为了简单起见,只是为了演示这个测试方法,我把你的组件换成一个简单的组件,原理是一样的。

例如

index.tsx:

import { useSelector, useDispatch } from 'react-redux';
import { useEffect } from 'react';
import React from 'react';

const getDetails = (id, otherId) => ({ type: 'GET_DETAILS' });

export const MyFunctionalComponent = () => {
  const { id, otherid } = useSelector((state: any) => state.user);
  const accounts = useSelector((state: any) => state.Selection.accounts);

  const dispatch = useDispatch();

  useEffect(() => {
    if (!accounts) {
      dispatch(getDetails(id, otherid));
    }
  }, []);

  if (!accounts) {
    return <div>CircularLoader</div>;
  }

  if (!accounts.length) {
    return <div>AccessUnavailablePanel</div>;
  }

  if (accounts.length === 1) {
    return <div>Redirect</div>;
  }

  return <div>MyList</div>;
};

index.test.tsx:

import { MyFunctionalComponent } from './';
import createMockStore from 'redux-mock-store';
import { screen, render } from '@testing-library/react';
import { Provider } from 'react-redux';
import React from 'react';

const mockStore = createMockStore([]);

describe('69013562', () => {
  test('should render AccessUnavailablePanel', () => {
    const state = {
      user: { id: '1', otherid: '2' },
      Selection: { accounts: [] },
    };
    const store = mockStore(state);
    render(<MyFunctionalComponent />, {
      wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
    });
    expect(screen.getAllByText('AccessUnavailablePanel')).toBeTruthy();
    expect(store.getActions()).toEqual([]);
  });

  test('should render CircularLoader and dispatch get details action', () => {
    const state = {
      user: { id: '1', otherid: '2' },
      Selection: {},
    };
    const store = mockStore(state);
    render(<MyFunctionalComponent />, {
      wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
    });
    expect(screen.getAllByText('CircularLoader')).toBeTruthy();
    expect(store.getActions()).toEqual([{ type: 'GET_DETAILS' }]);
  });

  test('should render Redirect', () => {
    const state = {
      user: { id: '1', otherid: '2' },
      Selection: { accounts: [1] },
    };
    const store = mockStore(state);
    render(<MyFunctionalComponent />, {
      wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
    });
    expect(screen.getAllByText('Redirect')).toBeTruthy();
    expect(store.getActions()).toEqual([]);
  });

  test('should render MyList', () => {
    const state = {
      user: { id: '1', otherid: '2' },
      Selection: { accounts: [1, 2, 3] },
    };
    const store = mockStore(state);
    render(<MyFunctionalComponent />, {
      wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
    });
    expect(screen.getAllByText('MyList')).toBeTruthy();
    expect(store.getActions()).toEqual([]);
  });
});

测试结果:

 PASS  examples/69013562/index.test.tsx (10.158 s)
  69013562
    ✓ should render AccessUnavailablePanel (28 ms)
    ✓ should render CircularLoader and dispatch get details action (3 ms)
    ✓ should render Redirect (2 ms)
    ✓ should render MyList (2 ms)

-----------|---------|----------|---------|---------|-------------------
File       | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------|---------|----------|---------|---------|-------------------
All files  |     100 |      100 |     100 |     100 |                   
 index.tsx |     100 |      100 |     100 |     100 |                   
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        11.014 s

测试策略没有单一答案,根据具体情况调整。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-01-15
    • 2019-11-11
    • 1970-01-01
    • 1970-01-01
    • 2020-05-15
    • 2016-05-09
    • 2020-01-11
    • 2019-09-30
    相关资源
    最近更新 更多