Mocking data for Integration tests in React
Tags: react, programming, testing, jest
Published: 2023-03-11 15:37:33

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](https://miro.medium.com/max/1400/0*OUKK80yPdRUd5ZwG)

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](https://testing-library.com/docs/react-testing-library/intro/)(13.4.0) for testing. Now we can start coding. 
Imagine you have component like this:

```tsx
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:
```tsx
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:
```tsx
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.
```tsx
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](https://mobx.js.org/README.html) for the state management and use `useStore` hook to request and manipulate data. Here is the updated version of the `UsersList` component.
```tsx
const UsersList = observer(() => {
  const {
    usersStore: { getUsers }
  } = useStore();

  useEffect(() => {
    getUsers();
  }, []);
  ...
});
```
Let's try to mock this hook and return data:
```tsx
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:
```tsx
(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. 
![Why?](https://media.giphy.com/media/11tTNkNy1SdXGg/giphy.gif)
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.
```tsx
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
```tsx
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:
```tsx
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.
```tsx
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:
```tsx
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:
```tsx
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.