Testing routing from one route to other route
// Add additional routes here to test routing
const UnitTestRouter = () => (
<Routes>
<Route
path={'user/*'}
element={<UserDashboard />}
/>
<Route
path={'user/add'}
element={
<AddUserProvider initialState={null}>
<AddUserConfig />
</AddUserProvider>
}
/>
</Routes>
);
const CustomRouterProvider = ({ initalRoute }: { initalRoute: string }) => (
<MemoryRouter initialEntries={[initalRoute]}>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
<ErrorBoundary>
<UnitTestRouter />
</ErrorBoundary>
</ThemeProvider>
</QueryClientProvider>
</MemoryRouter>
);
export const customRouterRender = (
component: JSX.Element,
initalRoute: string,
) =>
render(component, {
wrapper: () => <CustomRouterProvider initalRoute={initalRoute} />,
});
In the test file, use customRouterRender to load the initial route
it('should go to /user/add route and come back to dashboard when cancel is clicked', async () => {
const { getByText, findByPlaceholderText } = customRouterRender(<UserDashboard />, '/user');
// rest of the test
});
jest.mock vs jest.spyOn
jest.mock:
- Use this when you don't care about the internal implementation of the function/module.
- All you do is return some value from that function using mockImplementation for your test cases.
- This mock cannot be restored with original implementation in a specific test case we can wrap it in act() and render the component and then expect our cases
jest.spyOn:
- Use this to mock a function/module same as jest.mock but one difference in this is you can restore the mocked implementation to its original implementation for specific test case.
example - jest.spyOn:
import * as userModule from './user.api';
// create a mock for the function in a module
const mockFn = jest.spyOn(userModule, 'getUser');
// add your implementation to that mock
mockFn.mockImplementation(() => ({
name: 'jonsnow',
universe: 'game of thrones'
}));
it ('should display correct name', async () => {
// success
expect(userModule.getUser().name).toEqual('jonsnow');
// restore the implementation to its original
mockFn.restoreMock();
// get user from original implementation
expect(userModule.getUser()).not.toEqual('jonsnow')
});
If a component makes api call on first render (like fetching a resource based on id from route params)
we can wrap it in act() and render the component and then expect our cases
example:
it ('should load editable user', async () => {
await act(async () => {
render(<UserConfigForm userId={1} />);
});
// this will make sure to render the component
// do all the api/state related updates until the DOM is stable
expect(screen.queryByText('username').toEqual('barryallen')
});
Waiting for something
waitFor is used to let the tests know to wait till the expectation inside is met.
example:
it('should show name input field', async () => {
const { getByText, getByDisplayValue } = render(<ConfigForm />);
// waits till the expectation is met 👇
await waitFor(() => {
expect(getByText('Save')).not.toBeDisabled();
});
// expect other cases post waiting
expect(getByText('Name')).toBeInTheDocument();
expect(getByDisplayValue('Barry')).toBeInTheDocument();
});
Mocking services:
Mock services with expected response instead of actually making the network call
example:
profile.api.ts export different APIs. mock the file and later mock required function's implementation with an expected response
// on top of test file
jest.mock('../../../../services/user/info/profile.api', () => ({
getUserProfile: jest.fn(),
getAdminProfile: jest.fn(),
}));
// inside a specific test
(getUserProfile as jest.Mock).mockImplementation(() => ({
error: null,
data: {
id: 'user-id',
name: 'barry allen',
age: 27
},
}));
this mock implementation will return the object when the test case hits the api (no network request is made) 🚀
Also make sure to restore the mocks after each test, so that next test will have its own mock implementation
beforeEach(() => {
jest.resetAllMocks();
});
Helper function to get an element by name
const getSelector = (
container: HTMLElement,
key: keyof typeof formLables,
) => container.querySelector(`[name='${key}']`) as HTMLInputElement;
const { container } = renderWithProviders(<Component prop={value}>);
getSelector(container, 'username')
This will get the name=username input element
Helper function to fire onchange event on input
const fireInputChangeEvent = (
container: HTMLElement,
key: keyof typeof formLables,
value: string,
) => {
fireEvent.change(
container.querySelector(`[name='${key}']`) as HTMLInputElement,
{ target: { value } },
);
};
const { container } = renderWithProviders(<Component prop={value}>);
fireInputChangeEvent(container, 'username', 'barryallen')
This will emit an onchange event on name=username input with event.target.value=barryallen
If you have to test a button click event. And the click handler updates few states in the component, wrap the click event emitter in act
const saveButtonEl = getByText('Save');
await act(async () => {
fireEvent.click(saveButtonEl);
});
// states changes are applied to DOM
// make assertions based on the state updates
const successEl = getByText('user saved')
expect(successEl).toBeInTheDocument()
wrapping code with act will make sure all the state updates are done and rendered them to DOM. Post this you can make your assertions
Issue: The current testing environment is not configured to support act(...)
Solution: testing-library/react-testing-library#1061 (comment)
Test a button click and show validation under the input field
import { fireEvent, screen, waitFor } from '@testing-library/react';
const { getByText } = renderWithProviders(<Component prop={value}>);
const saveButtonEl = getByText('Save');
fireEvent.click(saveButtonEl);
await waitFor(() => {
const inputLabelEl = screen.queryByText('Username') as HTMLElement;
expect(
inputLabelEl.nextSibling?.firstChild?.lastChild?.textContent
).toEqual(
'invalid username
);
});
Test to type inside input and check the focus
import { act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
const { container } = renderWithProviders(<Component prop={value}>);
const emailEl = container.querySelector('#email') as HTMLInputElement;
// set focus inside email input
act(() => {
emailEl.focus();
});
// type characters inside email input
userEvent.type(emailEl, 'c');
// check if focus is still on email input
expect(document.activeElement).toBe(emailEl);