Eugen
Eugen

Reputation: 2990

How do I mock and test MaterialUI - makeStyles

I'm trying to add a better testing coverage for my React components, and one of the places I cannot mock is the inside of this

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

When I check the code coverage, is saying that the >>> line is not being checked. I've tried to have something like this

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

but then I'm not sure, how to check whether the given line was called with size = MD or LG.

Here is the code for it

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

Upvotes: 3

Views: 11056

Answers (4)

Sanat Gupta
Sanat Gupta

Reputation: 1154

I am getting the same issue so I resolve that issue like that I hope it's helpful for others as well.

Thanks

This is my style file

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

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

Here is my Component

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

Now here is the test case.

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();
    });
});

I just use ThemeProvider for the accessing theme variable inside html

Upvotes: 1

KeitelDOG
KeitelDOG

Reputation: 5170

Mocking makeStyles with simple jest function make you loose test coverage. When making it more complex, it leads to some problems, and each problem solved leads to another:

  • it lost test coverage when calling useStyles which is now an empty function with no style (const useStyles = makeStyles(theme => {...}))
  • Not mocking it throw error for additional values of a custom theme
  • Binding mocked function argument with the custom theme works, and you can call function argument to fill coverage. But you loose coverage if passing parameters when calling useStyles({ variant: 'contained', palette: 'secondary' }) (result function of makeStyles)
  • Lot of things broke when mocking useContext, because makeStyles result function uses useContext internally.

(example of useStyles parameter handling)

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

I managed to solved all of these problems and use manual mock https://jestjs.io/docs/en/manual-mocks:

Step 1:

I mocked in the core path instead, but both should work: <root>/__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 };

So basically, this is just using the same original makeStyles and passes it the custom theme on the go which was not ready on time.

Step 2:

makeStyles result uses React.useContext, so we have to avoid mocking useContext for makeStyles use cases. Either use mockImplementationOnce if you use React.useContext(...) at the first place in you component, or better just filter it out in your test code as:

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);
});

And on your createContext() call, probably in a store.js, add a displayName property (standard), or any other custom property to Identify your context:

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

The makeStyles context displayName will appear as StylesContext and ThemeContext if you log them and their implementation will remain untouched to avoid error.

This fixed all kind of mocking problems related to makeStyles + useContext. And in term of speed, it just feels like the normal shallow rendering speed and can keep you away of mount for most use cases.

ALTERNATIVE to Step 1:

Instead of global manual mocking, we can just use the normal jest.mock inside any test. Here is the implementation:

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));
    },
  };
});

Since then, I also learned to mock useEffect and calling callback, axios global interceptors, etc.

Upvotes: 2

quirimmo
quirimmo

Reputation: 9988

What is happening on the coverage side is that the function being tested, the hook useTabStyles is the result of makeStyles fn, which accepts as input a callback, which is the one missing the coverage because it does not get executed following your mock.

If you change your mock in this way, this should also execute that code which will be then cover:

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 });
}),

You can also anyway ignore the coverage checks of that fn simply adding before the following line:

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

Upvotes: 1

Julius Koronci
Julius Koronci

Reputation: 417

what about extracting it into a function and test it separately?

Upvotes: 0

Related Questions