Search code examples
reactjstypescriptjestjsreact-querytanstackreact-query

How to mock only the data except other information (isLoading, isError, etc.) returned from custom hook using useQuery?


Environment

  • Next.js v14 Pages Router
  • jest v29.7
  • @tanstack/react-query v5.36.0

Overview

There is a custom hook called useUserInfo that wraps useQuery from tanstack-query, and It fetches user information.
I want to test the useAccountMenu custom hook that uses that useUserInfo (each role of custom hooks will be explained later).
The return value of useUserInfo, that will be stored in data property of returend object (convention of tanstack-query), has the following type.

// type.ts
type ActiveUserInfo = {
  id: number,
  name: string,
  age: number,
}

And this is the implementation of the useUserInfo custom hook that wraps the useQuery.

// useUserInfo.ts
export const useUserInfo = () => {
  // assume fetchLoggedInUser is simple fetch()
  return useQuery(['loggedInUser'], fetchLoggedInUser);
};

And this is the useAccountMenu custom hook, which uses the useUserInfo custom hook to return data for the account menu link.
The return value of useAccountMenu changes depending on the value returned from useUserInfo.

// useAccountMenu.ts
export const useAccountMenu = () => {
  const {data} = useUserInfo();

  if (data.id > 100) { // this is example condition
    return {
      menuItems: [
        {menuLabel: 'User Setting', to: 'setting/user'},
        {menuLabel: 'Language', to: 'setting/language'},
      ]
    };
  };

  return {
    menuItems: [
      {menuLabel: 'User Setting', to: 'setting/user'},
      {menuLabel: 'Language', to: 'setting/language'},
      {menuLabel: 'Organization', to: 'setting/organization'},
    ]
  };
};

Test code that doesn't work well.

// useAccountMenu.test.ts
jest.mock('./useAccountMenu');

describe.concurrent('useAccountMenu', () => {
  it('returns all menu objects', async () => {
    const mockData = {
      id: 3,
      name: 'John',
      age: 25,
    };

    jest.mocked(useUserInfo).mockReturnValueOnce({ data: mockData }); // type Error!!

    const { result } = renderHook(() => useAccountMenu(), { wrapper });

    await waitFor(() => {
      expect(result.current.menuItems).toEqual([
        {menuLabel: 'User Setting', to: 'setting/user'},
        {menuLabel: 'Language', to: 'setting/language'},
        {menuLabel: 'Organization', to: 'setting/organization'},
      ]);
    });
  });

  // and here below will be test for the case of data.id > 100.
  // ...
})

The line mockReturnValueOnce is executed, I got a TypeError: Type '{ data: { id: string; name: string; age: number; }; }' is missing the following properties from type 'QueryObserverSuccessResult<ActiveUserInfo, Error>': error, isError, isPending, isLoading, and 19 more.ts(2345).

In this case, do I have to hardcode isError, isPending, etc. every time and create mock data?
Or is there a smarter solution or a different way of testing for useAccountMenu?
If this error can be fixed, it will relieve me of the stress I have been suffering from the last few days...
Thank you very much for your time.


Solution

  • The usual recommendation to test react query is to test a component that uses the hook. This is usually better because it tests what the user is seeing, not the implementation of the hook. So you can navigate to a page and see if the data is displayed correctly. This is also how all react-query tests are written internally.

    However, that still means the component that uses the hook will make a network request, which you likely don't want to do in tests. Two solutions come to mind for this:

    1. Mock the network layer

    You can let the fetch happen and intercept it with tools like msw or nock and return a response that fits your structure.

    1. You can seed the QueryCache

    Before rendering your component, you can call queryClient.setQueryData(key, data) to put some data into the cache before rendering. If you have a staleTime set, there will be no further request happening. You can also set a larger staleTime only for the queryClient that you are using in the test.