使用 useSelector 钩子测试反应组件

时间:2021-02-23 21:31:05

标签: reactjs typescript testing redux react-hooks

我在尝试使用 react redux 中的 useSelector 钩子为组件实现测试时遇到了很多麻烦。我已经看到一些关于这个主题的问题,但我没有设法使用针对这些问题的建议解决方案来解决我的问题。

我的组件非常大,所以我不会全部发布,但给我带来麻烦的部分看起来像这样:

Total.tsx

import React from 'react';

import clsx from 'clsx';
import i18next from 'i18next';
import { useSelector } from 'react-redux';
import { Trans } from 'react-i18next';

import Box from '@material-ui/core/Box';
import CustomTooltip from '../CustomTooltip/CustomTooltip';
import SkeletonTotal from 'components/Skeletons/Total';

import { ApplicationHelper } from 'helpers';

import './Total.scss';

//Some interfaces here for types since this is in TypeScript

function Total(props: TotalProps) {
  const { currency } = useSelector(
    (state: { currencyReducer: any }) => state.currencyReducer
  );
...
}

我首先尝试像另一个不使用 redux 的组件一样测试它:

Total.test.js(第一次尝试)

import React from 'react';
import Total from './Total';
import { render } from '@testing-library/react';

test('test', () => {
  const { container } = render(
    <Total priceLoading={false} bookingPrice={bookingPrice} values={myFormValues} />
  );
});

但是我收到一个错误,说我需要一个 react-redux 上下文值并将我的组件包装在一个 Provider 中,这让我尝试了这个:

Total.test.js(尝试 2)

import React from 'react';
import { Provider } from 'react-redux'
import Total from './Total';
import { render } from '@testing-library/react';

test('test', () => {
  const { container } = render(
    <Provider>
      <Total priceLoading={false} bookingPrice={bookingPrice} values={myFormValues} />
    </Provider>
  );
});

我现在收到 Provider 组件的“无法读取未定义的属性‘getState’”错误。我确实尝试模拟商店以传递给我的 Provider 以及使用 jest 模拟这样的返回值

const spy = jest.spyOn(redux, 'useSelector')
spy.mockReturnValue({currency: 'cad'})

不幸的是,我没有成功完成这项工作,并且在可能与此相关的其他问题中找不到可行的解决方案。有什么想法可以让我完成这项工作吗?谢谢

1 个答案:

答案 0 :(得分:1)

useSelector 钩子依赖于 redux 上下文来访问状态,因此它必须位于 Provider 组件内部才能工作。您的第二次尝试是在正确的轨道上,但您尚未在 store 上设置 Provider 道具,因此商店是 undefined 并且您收到错误 “无法读取属性'getState' of undefined".

由于您可能有许多要使用 redux 上下文进行测试的组件,因此 redux 文档 suggest 创建了您自己版本的 react 测试库的 render 函数,该函数将元素包装在提供者在呈现之前。这个新的 render 函数为 standard RTL options 添加了两个新的可选选项:initialStatestore

您基本上可以复制和粘贴整个 test-utils.js 示例 from the docs,但我修改了 return 以包含创建的 store,以便我们可以 dispatch直接发送给它(而不仅仅是与组件交互以分派动作)。

return {
  ...rtlRender(ui, { wrapper: Wrapper, ...renderOptions }),
  store
};

With typescript annotations

在组件测试文件中,您将使用 test-utils 来呈现 Total 组件。返回 container 元素是可以的,但实际上您不需要这样做,因为您可以在 queryglobal RTL screen object 上为您的基本元素bound queries 匹配元素。我们基本上希望看到输出的 HTML 代码符合预期。您可以单独测试选择器本身,但您似乎正在尝试测试组件。

您的测试可能如下所示:

import React from "react";
import Total from "./Total";
import { render, screen } from "./test-utils";
// if you want events: import userEvent from "@testing-library/user-event";

test( 'gets currency from redux', () => {
  // render with an initial currency
  const { store, container, getByLabelText } = render(
    // not sure where these props come from, presumable constants in the file
    <Total priceLoading={false} bookingPrice={bookingPrice} values={myFormValues} />,
    { initialState: { currency: USD } }
  );
  // some sort of RTL matcher or document.querySelector
  const currencyElement = getByLabelText(/currency/i); // uses regex for case-insensitivity
  // some sort of check for value
  expect(currencyElement?.innerText).toBe("USD");

  // dispatch an action to change the currency
  // might want to wrap in `act`? 
  store.dispatch(setCurrency("EUR"));
  
  // check that the value changed
  expect(currencyElement?.innerText).toBe("EUR");
});

Working example 是我基于基本计数器组件创建的。