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.