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?
Cannot write comments but you must note that what Alex Stoicuta said is wrong:
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)
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...In my opinion, you shouldn't worry about individually testing out methods inside the FC, rather testing it's side effects. eg:
Since you might be using useEffect which is async, you might want to wrap your expect in a setTimeout:
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:
You can refactor:
Helpers.js
LoginComponent.jsx
This way you could individually test
isPasswordValid
andisEmailValid
, and then when testing theLogin
component, you can mock your imports. And then the only things left to test for yourLogin
component would be that on click, the imported methods get called, and then the behaviour based on those response eg: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).Instead of isLoginDisabled state, try using the function directly for disabled. Eg.
When I was trying similar thing and was trying to check state(enabled/disabled) of the button from the test case, I didn't get the expected value for the state. But I removed disabled={isLoginDisabled} and replaced it with (password.length < 8 || !validateEmail(email)), it worked like a charm. P.S: I am a beginner with react, so have very limited knowledge on react.
Currently Enzyme doesn't support React Hooks and Alex's answer is correct, but looks like people (including myself) were struggling with using setTimeout() and plugging it into Jest.
Below is an example of using Enzyme shallow wrapper that calls useEffect() hook with async calls that results in calling useState() hooks.
Also, if you have nested describes with beforeEach() that interacts with component then you'll have to wrap beforeEach calls into withTimeout() as well. You could use the same helper without any modifications.
So by taking Alex's answer I was able to formulate the following method to test the component.
To test the state updates like Alex mentioned I tested for sideeffects:
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