Recently I had to write a lot of unit and integration tests with jest and react-testing-library for my front-end project. We increased the code coverage from 50% to 90%. I can only say that it was challenging, but one of the problems I faced was mocking some hooks, external functions, and components.
Below you can find the list of solutions I used and the conclusions I came to. If you know a better way to do the same â go ahead, I would be glad to hear your suggestions
Should I test my code?
Let's clarify this point first because I know that some developers have doubts about it.
I think testing your code not only makes sense but is also super-useful if you would like to build a reliable and maintainable application.
Testing your functions and components will help you to catch bugs early in the development process and ensure that they work as expected under different scenarios. It can also help identify issues with component dependencies and ensure that they are properly isolated and reusable.
So if you have any hesitation about it â throw it away, it will not harm you, on the contrary, It will significantly boost the understanding of your code!
The first test
Let me define the technologies that will be used in the examples below. For development are used React(18.2), Typescript(4.9.3) and Jest(^27) with React Testing Library(13.4.0) for testing. Now we can start coding.
Imagine you have component like this:
const Users = () => (
<Page testId={Id.UsersPage} className={classes.usersPage}>
<PageTitle title="Users" testId={Id.PageTitle} />
<UsersList testId={Id.UsersList} />
</Page>
);
We don't have any logic here, so we will check if the components are visible after we render them:
it('Users is rendered correctly', () => {
render(<Users />);
expect(screen.getByTestId(E2eId.UsersPage)).toBeVisible();
expect(screen.getByTestId(E2eId.PageTitle)).toBeVisible();
expect(screen.getByTestId(E2eId.UsersList)).toBeVisible();
});
Ok, so now we know that all the parts of the Users
component are rendered. Theoretically, we can test the markup instead, but it doesn't bright a lot of fruit because such a test will be fragile and will not check the business logic we might have in the components.
In real life, the test won't be that easy because within the UsersList
we probably have a request for the list of users the following way:
import { getUsers } from 'api';
const UsersList = observer(() => {
const [users, setUsers] = useState([]);
...
useEffect(() => {
getUsers().then((response: User[]) => {
setUsers(response)
});
}, []);
...
});
Looking at the component above, for testing Users
we need to mock API call here and jest.mock
will allow us to do that easily.
jest.mock('api', () => ({
getUsers: () => Promise.resolve([])
});
I'm not going to check if this function was called, but if it's necessary I have to use jest.fn
instead my current implementation.
Testing custom hooks
In terms of mocking the most frequent case for me, it is a custom hook that I used for getting data from the store. I selected MobX for the state management and use useStore
hook to request and manipulate data. Here is the updated version of the UsersList
component.
const UsersList = observer(() => {
const {
usersStore: { getUsers }
} = useStore();
useEffect(() => {
getUsers();
}, []);
...
});
Let's try to mock this hook and return data:
import { MOCKED_USERS } from 'mocks';
jest.mock('stores/store.context', () => ({
useStore: () => ({
usersStore: {
getUsers: jest.fn(() => Promise.resolve(MOCKED_USERS))
}
})
}))
If you try this, you will see that it doesn't work.
First of all, you can't use variables that are defined outside of the jest.mock
function. To solve it, you can try to hack jest.mock
by using nested functions, and in some cases, it works.
However, I would not rely on the such a solution and it won't help when you need to return the different datasets for different tests(Except if you want to use switch...case). So, after some experiments I came up with another idea and just imported getUsers
function in the test file and re-define mockImplementation the following way:
(getUsers as jest.Mock).mockImplementation(() => {
return Promise.resolve(MOCKED_USERS)
});
The only problem is ... it doesn't work as well!
But the reason is different â every time component is re-rendered we execute the hook function and return a new jest mock function. So when I re-defined it within the test, it was a different function.
So, we need to find a way of returning the same function all the time. Fortunately, it's not that difficult, we only need to define the mock function outside the hook function.
jest.mock('stores/store.context', () => {
const getUsersMock = jest.fn();
return {
useStore: () => {
usersStore: {
getUsers: getUsersMock
}
}
}
});
Let's take a look at what we will have in the test after all changes
it('triggers getUsers when component is mounted', async () => {
const {
usersStore: { getUsers }
} = useStore();
(getUsers as jest.Mock).mockImplementation(() => {
return Promise.resolve(MOCKED_USERS)
});
render(<UsersList />);
await waitFor(() => expect(screen.getByTestId(Id.UsersListContent)).toBeVisible());
expect(getUsers).toBeCalled();
});
Testing external hooks and dependencies
After we cracked the problem with hooks the rest cases don't seem difficult. Below I will go through pretty common situations when you need to mock external dependencies of the components. Imagine we have component:
import { useAuth0 } from '@auth0/auth0-react';
import TreeView from 'components/TreeView';
import { notification } from 'antd';
const SecurePage = observer(({ children }: SecurePageProps) => {
const { isAuthenticated, user } = useAuth0();
const onError = () => notification.open(
type: 'error',
message: 'Something went wrong'
});
...
return (
<div>{isAuthenticated ? children : <LoginPage onError={onError} />}
<TreeView />
</div>
);
});
To cover both authorized and unauthorized user scenarios, I will first mock auth0
. Then, I'll add a mock implementation of the useAuth0
hook in each of the tests. This approach will allow me to simulate different results and ensure that the app behaves correctly in each scenario.
jest.mock('@auth0/auth0-react');
...
(useAuth0 as jest.Mock).mockImplementation(() => ({
isAuthenticated: true,
user: {}
}))
So, we covered both cases when the user is authorized and not. But we probably would like to test showing notification on the error as well, so I will mock antd
and verify notification.open
is executed:
import { notification } from 'antd';
...
jest.mock('antd');
...
expect(notification.open).toBeCalled();
One more thing that might be helpful in some cases is mocking components. I have TreeView
in the SecurePage
component. Let's say I don't want to test it, I can mock it the following way:
jest.mock('components/TreeView', () => null)
Be careful we skipped the testing the TreeView
component entirely. I can only recommend it when the TreeView
component has already been tested separately or when you have a specific reason to omit it from certain tests.
Conclusion
Mocking data for your tests usually seems like something simple and developers don't pay a lot of attention to it. But in reality, it's a crucial technique for ensuring the trustworthiness and stability of your tests. Bad mocks can give you a deceptive sense of reliability, so you need to be careful.
On contrary, using mock data properly can help you isolate and debug problems more quickly and effectively. Having it in mind, you will create integration tests that are robust and accurate, and deliver high-quality user experiences for your customers.
Top comments (0)