Testing React Functional Component with Hooks using Jest
Cannot write comments but you must note that what Alex Stoicuta said is wrong:
setTimeout(() => {
expect(submitButton.prop('disabled')).toBeTruthy();
});
this assertion will always pass, because ... it's never executed. Count how many assertions are in your test and write the following, because only one assertion is performed instead of two. So check your tests now for false positive)
it('should fail',()=>{
expect.assertions(2);
expect(true).toEqual(true);
setTimeout(()=>{
expect(true).toEqual(true)
})
})
Answering your question, how do you test hooks? I don't know, looking for an answer myself, because for some reason the useLayoutEffect
is not being tested for me...
So by taking Alex's answer I was able to formulate the following method to test the component.
describe('<Login /> with no props', () => {
const container = shallow(<Login />);
it('should match the snapshot', () => {
expect(container.html()).toMatchSnapshot();
});
it('should have an email field', () => {
expect(container.find('Email').length).toEqual(1);
});
it('should have proper props for email field', () => {
expect(container.find('Email').props()).toEqual({
onBlur: expect.any(Function),
isValid: false,
});
});
it('should have a password field', () => {
expect(container.find('Password').length).toEqual(1);
});
it('should have proper props for password field', () => {
expect(container.find('Password').props()).toEqual({
onChange: expect.any(Function),
value: '',
});
});
it('should have a submit button', () => {
expect(container.find('Button').length).toEqual(1);
});
it('should have proper props for submit button', () => {
expect(container.find('Button').props()).toEqual({
disabled: true,
onClick: expect.any(Function),
});
});
});
To test the state updates like Alex mentioned I tested for sideeffects:
it('should set the password value on change event with trim', () => {
container.find('input[type="password"]').simulate('change', {
target: {
value: 'somenewpassword ',
},
});
expect(container.find('input[type="password"]').prop('value')).toEqual(
'somenewpassword',
);
});
but to test the lifecycle hooks I still use mount instead of shallow as it is not yet supported in shallow rendering. I did seperate out the methods that aren't updating state into a separate utils file or outside the React Function Component. And to test uncontrolled components I set a data attribute prop to set the value and checked the value by simulating events. I have also written a blog about testing React Function Components for the above example here: https://medium.com/@acesmndr/testing-react-functional-components-with-hooks-using-enzyme-f732124d320a
In my opinion, you shouldn't worry about individually testing out methods inside the FC, rather testing it's side effects. eg:
it('should disable submit button on submit click', () => {
const wrapper = mount(<Login />);
const submitButton = wrapper.find(Button);
submitButton.simulate('click');
expect(submitButton.prop('disabled')).toBeTruthy();
});
Since you might be using useEffect which is async, you might want to wrap your expect in a setTimeout:
setTimeout(() => {
expect(submitButton.prop('disabled')).toBeTruthy();
});
Another thing you might want to do, is extract any logic that has nothing to do with interacting with the form intro pure functions. eg: instead of:
setIsLoginDisabled(password.length < 8 || !validateEmail(email));
You can refactor:
Helpers.js
export const isPasswordValid = (password) => password.length > 8;
export const isEmailValid = (email) => {
const regEx = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return regEx.test(email.trim().toLowerCase())
}
LoginComponent.jsx
import { isPasswordValid, isEmailValid } from './Helpers';
....
const validateForm = () => {
setIsLoginDisabled(!isPasswordValid(password) || !isEmailValid(email));
};
....
This way you could individually test isPasswordValid
and isEmailValid
, and then when testing the Login
component, you can mock your imports. And then the only things left to test for your Login
component would be that on click, the imported methods get called, and then the behaviour based on those response
eg:
- it('should invoke isPasswordValid on submit')
- it('should invoke isEmailValid on submit')
- it('should disable submit button if email is invalid') (isEmailValid mocked to false)
- it('should disable submit button if password is invalid') (isPasswordValid mocked to false)
- it('should enable submit button if email is invalid') (isEmailValid and isPasswordValid mocked to true)
The main advantage with this approach is that the Login
component should just handle updating the form and nothing else. And that can be tested pretty straight forward. Any other logic, should be handled separately (separation of concerns).