Mocking data for Integration tests in React

mr_developer

Andy

Posted on March 11, 2023

Mocking data for Integration tests in React

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.
No tests — no problems

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>
);
Enter fullscreen mode Exit fullscreen mode

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();  
});
Enter fullscreen mode Exit fullscreen mode

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)
     });   
  }, []);
  ...
});
Enter fullscreen mode Exit fullscreen mode

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([])
});
Enter fullscreen mode Exit fullscreen mode

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();
  }, []);
  ...
});
Enter fullscreen mode Exit fullscreen mode

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))
    }
  })
}))
Enter fullscreen mode Exit fullscreen mode

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)
});
Enter fullscreen mode Exit fullscreen mode

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.
Why?
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
        }
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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>
  );
});
Enter fullscreen mode Exit fullscreen mode

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: {}
}))
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
mr_developer
Andy

Posted on March 11, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related