Reputation: 2990
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
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
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:
const useStyles = makeStyles(theme => {...})
)useStyles({ variant: 'contained', palette: 'secondary' })
(result function of makeStyles)(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
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
Reputation: 417
what about extracting it into a function and test it separately?
Upvotes: 0