【问题标题】:How do I mock and test MaterialUI - makeStyles如何模拟和测试 MaterialUI - makeStyles
【发布时间】:2021-02-14 03:03:59
【问题描述】:

我正在尝试为我的 React 组件添加更好的测试覆盖率,而我无法模拟的地方之一就是它的内部

export const useTabStyles = makeStyles(({ options: { common } }) => ({
>>>  root: ({ size }: TabProps) => ({
    '&&': {
      fontSize: size === 'MD' ? common.fonts.sizes.p3 : common.fonts.sizes.p,
    },
  }),
}));

当我检查代码覆盖率时,是说>>> 行没有被检查。 我试过有这样的东西

jest.mock('@material-ui/core/styles', () => ({
  ...jest.requireActual('@material-ui/core/styles'),
  makeStyles: jest.fn().mockReturnValue(jest.fn()),
}));

但是我不确定如何检查给定的行是否使用size = MD or LG 调用。

这是it的代码

it('should render normal style', () => {
    wrapper = shallow(<Tab size="MD" />);
    // how do I mock check here whtehr the makeStyles received the proepr size.
  });

【问题讨论】:

  • 如果您仍然想要答案,我刚刚找到了模拟 makeStyles 的完美方法,直接使用 jest.mock 或手动模拟配置。这解决了所有 4 个主要问题:1- 未定义的主题,2- 正常测试覆盖率,3- 在 useStyles 中传递参数并在 makeStyles 函数属性中接收它们时的高级测试覆盖率,4- useStyles() 模拟时出错 React.context 搞砸了makeStyles 内部使用 React.context 的函数结果。
  • @KeitelDOG 这里没有接受的答案,所以你应该把它放在这里作为答案。
  • @user1713450 我认为你是对的。我刚刚添加了一个答案。

标签: reactjs unit-testing jestjs material-ui enzyme


【解决方案1】:

我遇到了同样的问题,所以我解决了这个问题,我希望它对其他人也有帮助。

谢谢

这是我的样式文件

import { makeStyles } from '@material-ui/core/styles';

export const useStyles = makeStyles((theme) => ({
   root:{
       backgroundColor: theme.common.white,
   }
}));

这是我的组件

import { useStyles } from './ExampleStyles';
const Example = ({ children }) => {
    const classes = useStyles();
    return (
       <div className={classes.root}><h4>Hello world!</h4></div>
    );
};
export default Example;

现在是测试用例。

import { ThemeProvider } from '@material-ui/core/styles';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import Enzyme, { mount } from 'enzyme';
import renderer from 'react-test-renderer';
import Example from 'shared/components/Example';
import theme from 'shared/utils/theme';
Enzyme.configure({ adapter: new Adapter() });

describe('Example Component', () => {
    const props = {};
    it('Should render Example component', () => {
        const wrapper = mount(
            <ThemeProvider theme={theme}>
                <Example {...props} />
            </ThemeProvider>
        );
        expect(wrapper).toBeTruthy();
    });

    it('Example Component snapshot testing', () => {
        const div = document.createElement('div');
        const tree = renderer
            .create(
                <ThemeProvider theme={theme}>
                    <Example {...props} />
                </ThemeProvider>,
                div
            )
            .toJSON();
        expect(tree).toMatchSnapshot();
    });
});

我只是使用 ThemeProvider 来访问 html 中的主题变量

【讨论】:

  • 这是一个非常好的方法。但问题是,当在许多测试中多次执行此操作时,Mount 会花费太多时间用于顶级组件,并且大多数人希望远离并使用 Shallow,这将需要模拟一些 Material UI 功能以使用浅渲染。但是如果项目不大,那么Mount也不是问题。
【解决方案2】:

使用简单的 jest 函数模拟 makeStyles 会使您失去测试覆盖率。当它变得更复杂时,它会导致一些问题,并且每个解决的问题都会导致另一个问题:

  • 在调用 useStyles 时失去了测试覆盖率,useStyles 现在是一个没有样式的空函数 (const useStyles = makeStyles(theme =&gt; {...}))
  • 不模拟自定义主题的附加值会引发错误
  • 将模拟函数参数与自定义主题绑定有效,您可以调用函数参数来填充覆盖范围。但是如果在调用useStyles({ variant: 'contained', palette: 'secondary' })(makeStyles 的结果函数)时传递参数,则会失去覆盖率
  • 在模拟 useContext 时发生了很多事情,因为 makeStyles 结果函数在内部使用了 useContext。

(useStyles参数处理示例)

{
  backgroundColor: props => {
    if (props.variant === 'contained') {
      return theme.palette[props.palette].main;
    }
    return 'unset';
  },
}

我设法解决了所有这些问题并使用手动模拟https://jestjs.io/docs/en/manual-mocks

第 1 步:

我在核心路径中进行了模拟,但两者都应该工作:&lt;root&gt;/__mocks__/@material-ui/core/styles.js

// Grab the original exports
// eslint-disable-next-line import/no-extraneous-dependencies
import * as Styles from '@material-ui/core/styles';
import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
import options from '../../../src/themes/options'; // I put the theme options separately to be reusable

const makeStyles = func => {
  /**
   * Note: if you want to mock this return value to be
   * different within a test suite then use
   * the pattern defined here:
   * https://jestjs.io/docs/en/manual-mocks
   */

  /**
   * Work around because Shallow rendering does not
   * Hook context and some other hook features.
   * `makeStyles` accept a function as argument (func)
   * and that function accept a theme as argument
   * so we can take that same function, passing it as
   * parameter to the original makeStyles and
   * bind it to our custom theme, created on the go
   *  so that createMuiTheme can be ready
   */
  const theme = createMuiTheme(options);
  return Styles.makeStyles(func.bind(null, theme));
};

module.exports = { ...Styles, makeStyles };

所以基本上,这只是使用相同的原始makeStyles,并在旅途中将未按时准备好的自定义主题传递给它。

第 2 步:

makeStyles 结果使用 React.useContext,因此我们必须避免在 makeStyles 用例中模拟 useContext。如果您在组件的第一个位置使用React.useContext(...),请使用 mockImplementationOnce,或者最好在测试代码中将其过滤掉:

jest.spyOn(React, 'useContext').mockImplementation(context => {
  // only stub the response if it is one of your Context
  if (context.displayName === 'MyAppContext') {
    return {
      auth: {},
      lang: 'en',
      snackbar: () => {},
    };
  }

  // continue to use original useContext for the rest use cases
  const ActualReact = jest.requireActual('react');
  return ActualReact.useContext(context);
});

在您的 createContext() 调用中,可能在 store.js 中,添加一个 displayName 属性(标准)或任何其他自定义属性来识别您的上下文:

const store = React.createContext(initialState);
store.displayName = 'MyAppContext';

如果您记录 makeStyles 上下文 displayName 将显示为 StylesContext 和 ThemeContext 并且它们的实现将保持不变以避免错误。

这修复了与 makeStyles + useContext 相关的所有类型的模拟问题。而且在速度方面,感觉就像普通的shallow 渲染速度,并且在大多数用例中可以让您远离mount

第 1 步的替代方案:

我们可以在任何测试中使用普通的jest.mock,而不是全局手动模拟。这是实现:

jest.mock('@material-ui/core/styles', () => {
  const Styles = jest.requireActual('@material-ui/core/styles');

  const createMuiTheme = jest.requireActual(
    '@material-ui/core/styles/createMuiTheme'
  ).default;

  const options = jest.requireActual('../../../src/themes/options').default;

  return {
    ...Styles,
    makeStyles: func => {
      const theme = createMuiTheme(options);
      return Styles.makeStyles(func.bind(null, theme));
    },
  };
});

从那以后,我也学会了mockuseEffect和调用callback、axios全局拦截器等。

【讨论】:

    【解决方案3】:

    在覆盖方面发生的事情是正在测试的函数,钩子useTabStylesmakeStyles fn 的结果,它接受一个回调作为输入,这是因为它没有得到而缺少覆盖的回调在你的模拟之后执行。

    如果您以这种方式更改模拟,这也应该执行将覆盖的代码:

    makeStyles: jest.fn().mockImplementation(callback => {
      callback({ options: { common: { fonts: { sizes: {} } } } }); // this will execute the fn passed in which is missing the coverage
      return jest.fn().mockReturnValue({ // here the expected MUI styles });
    }),
    

    您也可以忽略该 fn 的覆盖检查,只需在以下行之前添加:

    /* istanbul ignore next */
    export const useTabStyles = makeStyles(({ options: { common } }) => ({
      root: ({ size }: TabProps) => ({
        '&&': {
          fontSize: size === 'MD' ? common.fonts.sizes.p3 : common.fonts.sizes.p,
        },
      }),
    }));
    
    

    【讨论】:

    • 谢谢!很好的收获,我唯一缺少的就是全面报道。这个应该被接受的答案。我在全局范围内手动模拟 makeStyles,随时随地创建主题并将其绑定到原始 makeStyles,因为自定义主题在某些地方尚未在 Hook 中准备好。我在一些测试中造成了很多失败。现在唯一的方法是调用回调函数,将创建的主题作为参数传递,BOOM: No Crash and Full Coverage。
    【解决方案4】:

    将其提取到一个函数中并单独测试呢?

    【讨论】:

    • 顺便说一句,您也可以询问您的同事..这样他们就可以在 stackoverflow 之外回复您:D
    猜你喜欢
    • 1970-01-01
    • 2018-02-07
    • 2019-07-20
    • 2020-07-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多