So I'm moving away from class based components to functional components but am stuck while writing test with jest/enzyme for the methods inside the functional components which explicitly uses hooks. Here is the stripped down version of my code.
function validateEmail(email: string): boolean { return email.includes('@'); } const Login: React.FC<IProps> = (props) => { const [isLoginDisabled, setIsLoginDisabled] = React.useState<boolean>(true); const [email, setEmail] = React.useState<string>(''); const [password, setPassword] = React.useState<string>(''); React.useLayoutEffect(() => { validateForm(); }, [email, password]); const validateForm = () => { setIsLoginDisabled(password.length < 8 || !validateEmail(email)); }; const handleEmailChange = (evt: React.FormEvent<HTMLFormElement>) => { const emailValue = (evt.target as HTMLInputElement).value.trim(); setEmail(emailValue); }; const handlePasswordChange = (evt: React.FormEvent<HTMLFormElement>) => { const passwordValue = (evt.target as HTMLInputElement).value.trim(); setPassword(passwordValue); }; const handleSubmit = () => { setIsLoginDisabled(true); // ajax().then(() => { setIsLoginDisabled(false); }); }; const renderSigninForm = () => ( <> <form> <Email isValid={validateEmail(email)} onBlur={handleEmailChange} /> <Password onChange={handlePasswordChange} /> <Button onClick={handleSubmit} disabled={isLoginDisabled}>Login</Button> </form> </> ); return ( <> {renderSigninForm()} </>); }; export default Login;
I know I can write tests for validateEmail
by exporting it. But what about testing the validateForm
or handleSubmit
methods. If it were a class based components I could just shallow the component and use it from the instance as
const wrapper = shallow(<Login />); wrapper.instance().validateForm()
But this doesn't work with functional components as the internal methods can't be accessed this way. Is there any way to access these methods or should the functional components be treated as a blackbox while testing?
So to make a function component with hooks testable, the function which triggers the state change: Has to be available as a prop so we can trigger it manually. Needs a mocked event if it uses values from it. Has to return a Promise if the state change happens async.
Testing React Hooks with Jest and Enzyme. Jest and Enzyme are tools used for testing React apps. Jest is a JavaScript testing framework used to test JavaScript apps, and Enzyme is a JavaScript testing utility for React that makes it easier to assert, manipulate, and traverse your React components' output.
Hooks are functions that let you “hook into” React state and lifecycle features from function components. Hooks don't work inside classes — they let you use React without classes. (We don't recommend rewriting your existing components overnight but you can start using Hooks in the new ones if you'd like.)
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:
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()) }
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With