Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing React Functional Component with Hooks using Jest

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?

like image 750
acesmndr Avatar asked Feb 15 '19 16:02

acesmndr


People also ask

How do you test a functional component with a hook?

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.

Can you test react hooks with Jest?

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.

Can we use react hooks in functional component?

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.)


1 Answers

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).

like image 199
Alex Stoicuta Avatar answered Oct 09 '22 20:10

Alex Stoicuta