Testing React Functional Component with Hooks using Jest - reactjs

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?

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

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

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.
// This is helper that I'm using to wrap test function calls
const withTimeout = (done, fn) => {
const timeoutId = setTimeout(() => {
fn();
clearTimeout(timeoutId);
done();
});
};
describe('when things happened', () => {
let home;
const api = {};
beforeEach(() => {
// This will execute your useEffect() hook on your component
// NOTE: You should use exactly React.useEffect() in your component,
// but not useEffect() with React.useEffect import
jest.spyOn(React, 'useEffect').mockImplementation(f => f());
component = shallow(<Component/>);
});
// Note that here we wrap test function with withTimeout()
test('should show a button', (done) => withTimeout(done, () => {
expect(home.find('.button').length).toEqual(1);
}));
});
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.

Instead of isLoginDisabled state, try using the function directly for disabled.
Eg.
const renderSigninForm = () => (
<>
<form>
<Email
isValid={validateEmail(email)}
onBlur={handleEmailChange}
/>
<Password
onChange={handlePasswordChange}
/>
<Button onClick={handleSubmit} disabled={(password.length < 8 || !validateEmail(email))}>Login</Button>
</form>
</>);
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.

Related

How to test onClick with Jest that is NOT a callback function in props?

I found lots of ways of using mock functions in jest to spy on callback functions that are passed down to a component but nothing on testing a simple onClick that is defined in the same component.
My Example Page:
const ExamplePage: NextPage = () => {
const router = useRouter();
const onClick = (): Promise<void> => {
axios.post(`/api/track`, {
eventName: Event.TRACK_CLICK,
});
router.push("/new-route");
return Promise.resolve();
};
return (
<Container data-testid="container">
<Title>Example Title</Title>
<CreateButton data-testid="create-button" onClick={onClick}>
Create Partner
</CreateButton>
</Container>
);
};
export default ExamplePage;
My current test where I am attempting to get the onClick from getAttribute:
import { fireEvent, render } from "../../../../test/customRenderer";
import ExamplePage from "../../../pages/example-page";
describe("Example page", () => {
it("has a button to create", () => {
const { getByTestId } = render(<ExamplePage />);
const createButton = getByTestId("create-button");
expect(createButton).toBeInTheDocument();
});
it(" the button's OnClick function should be executed when clicked", () => {
const { getByTestId } = render(<ExamplePage />);
// find the button
const createButton = getByTestId("create-button");
// check the button has onClick
expect(createButton).toHaveAttribute("onClick");
// get the onClick function
const onClick = createButton.getAttribute("onClick");
fireEvent.click(createButton);
// check if the button's onClick function has been executed
expect(onClick).toHaveBeenCalled();
});
});
The above fails since there is no onClick attribute only null. My comments in the test highlight my thought process of trying to reach down into this component for the function on the button and checking if it has been called.
Is there any way to test a onClick that is self contained in a react component?
You need to provide mocked router provider and expect that a certain route is pushed to the routers. You also need extract the RestAPI into a separate module and mock it! You can use Dependency Injection, IOC container or import the Api in the component and mock it using jest. I will leave the RestAPi mocking to you.
Mocking router details here: How to mock useRouter
const useRouter = jest.spyOn(require('next/router'), 'useRouter')
describe("", () => {
it("",() => {
const pushMock = jest.fn();
// Mocking Rest api call depends on how you are going to "inject it" in the component
const restApiMock = jest.jn().mockResolvedValue();
useRouter.mockImplementationOnce(() => ({
push: pushMock,
}))
const rendrResult = render(<ExamplePage />);
//get and click the create button
//expect the "side" effects of clicking the button
expect(restApiMock).toHaveBeenCalled();
expect(pushMock).toHaveBeenCalledWith("/new-route");
});
});

Is it necessary to call unmount after each test cases in react testing libarary?

describe('<CustomTooltip />', () => {
it('should show tooltip text', async () => {
const { container, unmount } = render(<CustomTooltip text='Tooltip Text' />)
userEvent.hover(container.querySelector('svg'))
await screen.findByText('Tooltip Text')
screen.debug()
unmount() // ?? is it necessary to call unmount after each test cases?
})
it('check if there is an mounted component', () => {
screen.debug()
})
})
Is it necessary to call unmount after each test cases? Because I've added useEffect in the CustomTooltip component, and Unmounted is logged before the second test case. And even the second test case screen.debug output is <body />.
useEffect(() => {
console.log('Mounted')
return () => console.log('Unmounted')
}, [])
I asked this because i saw a custom implementation for render in test utils to unmount component after each test cases, and I'm curious to know if this is really important.
let lastMount = null;
const _render = (...args) => {
lastMount = render(...args);
return lastMount;
};
afterEach(() => {
if (lastMount)
lastMount.unmount();
lastMount = null;
});
It depends on the testing framework that you are using. If you are using jest, for instance, it's not needed.
Here is a reference to this suggestion https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#using-cleanup
For a long time now cleanup happens automatically (supported for most major testing frameworks) and you no longer need to worry about it

How to use jest.spyOn with react testing library

I'm refactoring a class component to a funcional component.
In my test file i was using enzyme, but i'm migrating to react-testing-library
This the test i'm doing using enzyme
it('should change module when clicked button login', () => {
const wrapper = mount(<SignUp setModuleCallback={jest.fn()} />)
const instance = wrapper.instance()
jest.spyOn(instance, 'setModule')
wrapper.find('button#login-button').simulate('click')
expect(instance.setModule).toHaveBeenCalled()
})
And this is what i'm trying to do using react-testing-library
it('should change module when clicked button login', async () => {
const { getByTestId } = render(<SignUp setModuleCallback={jest.fn()} />)
const instance = getByTestId('submit')
jest.spyOn(instance, 'setModule')
const button = await waitFor(() => getByTestId('submit'))
act(() => {
fireEvent.click(button)
})
expect(instance.setModule).toHaveBeenCalled()
})
Here's the error that i'm getting
The philosophy behind RTL is that you should test your components "the way your software is used." With that in mind, is there a way to test your component without explicitly asserting that the callback was invoked?
According to your test name, you expect some "module to change." Is there some way to verify in the DOM that the module was changed? For example, maybe each "module" has a unique title, in which case you could use screen.getByText to assert that the correct module is rendered after clicking the button.
If you want to explicitly assert that a callback function was invoked, are you sure you have to use spyOn for this test? I would try something like this:
it('should change module when clicked button login', async () => {
const mockCallback = jest.fn()
const { getByTestId } = render(<SignUp setModuleCallback={mockCallback} />)
const button = await waitFor(() => getByTestId('submit'))
act(() => {
fireEvent.click(button)
})
expect(mockCallback).toHaveBeenCalled()
})

Jest test case for UseEffect hooks in react JS

I am trying to write the Jest-enzyme test case for useEffect react hooks, and I am really lost, I want to write test case for 2 react hooks, one making the async call and another sorting the data and setting the data using usestate hooks, my file is here.
export const DatasetTable: React.FC<DatasetTableProps> = ({id, dataset, setDataset, datasetError, setDataSetError}) => {
const [sortedDataset, setSortedDataset] = useState<Dataset[]>();
useEffect(() => {
fetchRegistryFunction({
route:`/dataset/report?${constructQueryParams({id})}`,
setData: setDataset,
setError: setDataSetError
})();
}, [id, setDataset, setDataSetError]});
useEffect(() => {
if(dataset) {
const sortedDatasetVal = [...dataset];
sortedDatasetVal.sort(a, b) => {
const dateA: any = new Date(a.date);
const dateA: any = new Date(a.date);
return dataA - dateB;
}
setSortedDataset(sortedDatasetVal);
}
}, [dataset])
return (
<div>
<DatasetTable
origin="Datasets"
tableData={sortedDataset}
displayColumns={datasetColumns}
errorMessage={datasetError}
/>
</div>
);
}
Enzyme isn't the right library for this kind of testing.
https://react-hooks-testing-library.com/ is what you need.
In your case I would extract all the data fetching to a 'custom hook' and then test this independently from your UI presentation layer.
In doing so you have better separation of concerns and your custom hook can be used in other similar react components.
I managed to get enzyme to work with a data fetching useEffect hook. It does however require that you allow your dataFetching functions to be passed as props to the component.
Here's how I would go about testing your component, considering it now accepts fetchRegistryFunction as a prop:
const someDataSet = DataSet[] // mock your response object here.
describe('DatasetTable', () => {
let fetchRegistryFunction;
let wrapper;
beforeEach(async () => {
fetchRegistryFunction = jest.fn()
.mockImplementation(() => Promise.resolve(someDataSet));
await act(async () => {
wrapper = mount(
<DatasetTable
fetchRegistryFunction={fetchRegistryFunction}
// ... other props here
/>,
);
});
// The wrapper.update call changes everything,
// act seems to not automatically update the wrapper,
// which lets you validate your old rendered state
// before updating it.
wrapper.update();
});
afterEach(() => {
wrapper.unmount();
jest.restoreAllMocks();
});
it('should display fetched data', () => {
expect(wrapper.find(DatasetTable).props().tableData)
.toEqual(someDataSet);
});
});
Hope this helps!

How to mock or assert whether window.alert has fired in React & Jest with typescript?

I am using jest tests to test my React project written in #typescript created with Create React App. I'm using react-testing-library. I have a form component which shows an alert if the form is submitted empty. I wanted to test this feature (maybe) by spying/mocking the window.alert but it doesn't work.
I tried using jest.fn() as suggested in many SO answers but that's not working too.
window.alert = jest.fn();
expect(window.alert).toHaveBeenCalledTimes(1);
Here's how I implemented it: Form.tsx
async handleSubmit(event: React.FormEvent<HTMLFormElement>) {
// check for errors
if (errors) {
window.alert('Some Error occurred');
return;
}
}
Here's how I built the React+Jest+react-testing-library test: Form.test.tsx
it('alerts on submit click', async () => {
const alertMock = jest.spyOn(window,'alert');
const { getByText, getByTestId } = render(<Form />)
fireEvent.click(getByText('Submit'))
expect(alertMock).toHaveBeenCalledTimes(1)
})
I think you might need to tweak your test ever so slightly by adding .mockImplementation() to your spyOn like so:
it('alerts on submit click', async () => {
const alertMock = jest.spyOn(window,'alert').mockImplementation();
const { getByText, getByTestId } = render(<Form />)
fireEvent.click(getByText('Submit'))
expect(alertMock).toHaveBeenCalledTimes(1)
})
You could try to use global instead of window:
global.alert = jest.fn();
expect(global.alert).toHaveBeenCalledTimes(1);
Alternatively, try to Object.assign
const alert = jest.fn()
Object.defineProperty(window, 'alert', alert);

Resources