Is there any solution to mock react-query's useQuery and useMutation while working with react testing library
Asked Answered
I

7

31

I'm working on test cases with react testing library. To write the test case I need to mock the useQuery and useMutation method of react query. If anyone knows the solution please guide me. I'm adding the related code here.

WorkspaceDetailsSection.test.tsx

import React from 'react'
import '@testing-library/jest-dom'
import '@testing-library/jest-dom/extend-expect'
import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { WorkspaceDetailsSection } from '../WorkspaceDetailsSection'
import { render } from '../../_utils/test-utils/test-utils'

const mockedFunction = jest.fn()

let realUseContext: any
let useContextMock: any

// Setup mock
beforeEach(() => {
  realUseContext = React.useContext
  useContextMock = React.useContext = jest.fn()
})
// Cleanup mock
afterEach(() => {
  React.useContext = realUseContext
})

jest.mock('../data', () => ({
  useMutationHook: () => ({ mutate: mockedFunction })
}))

const WorkspaceContext = {
  workspaceInfo: {
    name: 'name',
    dot: 'name',
    type: 'type'
  }
}
test('renders section with the valid details', async () => {
  useContextMock.mockReturnValue(WorkspaceContext)

  render(<WorkspaceDetailsSection />)

  expect(screen.getByText('Workspace name:')).toBeInTheDocument()
})

WorkspaceDetailsSection.tsx

import React, { useContext } from 'react'

import { FormModal, useDisclosure } from '@chaine/keychaine'
import { workspaceService } from './data'
import { useMutation } from 'react-query'
import { Section } from '../account-settings'
import { Toast } from '../_shared/components'
import { IWorkspace } from '../_shared/refactoredInterfaces'
import constant from '../_shared/constants/message'
import { WorkspaceContext } from './WorkspacePage'
import capitalizeFirstLetter from '../_utils/capitalizeFirstLetter'
import { queryClient } from '../_shared/infra'
/**
 *
 * should import section component and return that
 */
export const WorkspaceDetailsSection = () => {
  const { isOpen, onOpen, onClose } = useDisclosure()
  const { workspaceInfo } = useContext(WorkspaceContext)
  const { name, dot, type } = workspaceInfo

  const { showToast } = Toast()

  const updateWorkspace = useMutation((params: any) => workspaceService.updateWorkspace(params))

  const handleSubmit = async (event: any) => {
    const params: IWorkspace = {
      ...event,
      id: workspaceInfo.id,
      type: event.type.value
    }

    updateWorkspace.mutate(params, {
      onSuccess: () => {
        onClose()
        queryClient.invalidateQueries('WorkspaceProfile')
      },
      onError: () => {
        showToast({
          title: constant.UNABLE_TO_UPDATE_WORKSPACE,
          status: 'error'
        })
      }
    })
  }

  return (
    <>
      <Section
        sectionTitle={'Workspace details'}
        multipleFields={[
          { fieldName: 'Workspace name:', value: name },
          { fieldName: 'Workspace type:', value: type },
          { fieldName: 'DOT #:', value: dot }
        ]}
        buttonName={'Edit details'}
        onClick={() => onOpen()}
      />

      <FormModal
        isOpen={isOpen}
        onClose={() => onClose()}
        title={'Change workspace info'}
        size={'lg'}
        formSubmitHandler={handleSubmit}
        isLoading={updateWorkspace.isLoading}
        initialValues={{
          dot: dot,
          type: { label: capitalizeFirstLetter(type), value: type },
          name: name
        }}
        modalItems={[
          {
            name: 'name',
            type: 'input',
            placeHolder: 'Enter you name',
            labelText: 'Workspace display name'
          },
          { name: 'dot', type: 'input', placeHolder: 'Enter you DOT #', labelText: 'DOT #' },
          {
            name: 'type',
            type: 'select',
            placeHolder: type,
            labelText: 'Workspace type',
            selectOptions: [
              { label: 'Carrier', value: 'carrier' },
              { label: 'Broker', value: 'broker' },
              { label: 'Shipper', value: 'shipper' }
            ]
          }
        ]}
      />
    </>
  )
}

WorkspaceService.ts

import { BaseAPI } from '../../_shared/infra/services/BaseAPI'
import { ITokenService, IWorkspace } from '../../_shared/refactoredInterfaces'
import { TeamResponseDTO, UpdateWorkspaceResponseDTO, UploadWorkspaceLogoResponseDTO } from './WorkspaceDTO'

export interface IWorkspaceService {
  getWorkspace(): Promise<TeamResponseDTO>
  updateWorkspace(params: IWorkspace): Promise<UpdateWorkspaceResponseDTO>
  uploadWorkspaceLogo(params: any): Promise<UploadWorkspaceLogoResponseDTO>
}

export class WorkspaceService extends BaseAPI implements IWorkspaceService {
  constructor(tokenService: ITokenService) {
    super(tokenService)
  }

  async getWorkspace(): Promise<TeamResponseDTO> {
    const body = {
      url: '/users/account'
    }

    const { data } = await this.get(body)
    return {
      team: data?.data,
      message: data?.message
    }
  }

  async updateWorkspace(params: IWorkspace): Promise<UpdateWorkspaceResponseDTO> {
    const body = {
      url: '/accounts',
      data: params
    }
    const { data } = await this.put(body)
    return {
      message: data.message
    }
  }

  async uploadWorkspaceLogo(params: FormData): Promise<UploadWorkspaceLogoResponseDTO> {
    const body = {
      url: '/accounts/logos',
      data: params,
      headers: {
        'Content-Type': 'multipart/form-data'
      }
    }
    const response = await this.post(body)

    return response
  }
}

Also tried the solution proposed by @TkDodo here but did not work for me. The solution of this will be a life saver for me so thank you folks in advance.

Immersionism answered 10/1, 2022 at 14:50 Comment(1)
I don't think it makes much sense to tell you then that mocking the whole hook is not the best approach and mocking the network request only is likely better. But you also haven't shown what the issue with your code is? What isn't working, and what hasn't worked when you tried to mock the network request? The official docs also have a straight forward example with nock: react-query.tanstack.com/guides/testing#testing-network-callsRuthannruthanne
F
13

The way that I found that works, is to spy the react-query module and mock the implementation of useQuery.

import * as ReactQuery from 'react-query'
    
    jest
      .spyOn(ReactQuery, 'useQuery')
      .mockImplementation(
        jest
          .fn()
          .mockReturnValue({ data: { ...MockData }, isLoading: false, isSuccess: true })
      )
Femmine answered 24/1, 2023 at 11:24 Comment(0)
P
11

This worked for me

jest.mock('react-query', () => ({
 useQuery: jest.fn().mockReturnValue(({ data: {...MockData}, isLoading: false,error:{} }))
}));
Pacificism answered 29/7, 2022 at 11:8 Comment(2)
I would love to see this in context - any repo you can share?Giraudoux
could you include the required imports?Jilli
Q
4

I don't have enough reputation to reply to @TkDodo's comment above (the "price" of mostly lurking around on SO, I suppose 😅), but I have found that mocking react-query's useQuery() return value is definitely useful for exercising specific rendering outcomes without having to bother with async tests littered with await waitFor()s all over.

Sadly, I have not been able to find a ready-made package that manages this for me.

That said, I can report that I've found value in having created a few helper functions for my unit tests that produce an object with the de-structured keys documented here.

The function signatures are as follows:

export function generateUseQueryReturnValueError(error, overrides = {}) {
  // use the error argument as the reported error object and make sure to
  // set flags like `isSuccess`, `isError`, `isLoading` and the rest...
}

export function generateUseQueryReturnValueSuccess(data, overrides = {}) {
  // use the data argument as you'd expect and, obviously, set the flags
}

export function generateUseQueryReturnValueLoading(overrides = {}) {
  // same note about setting the flags correctly
}

It's worth nothing, however, that although these functions do some (very light!) "sanity" checking / prevention, like ensuring that the isError field cannot be overridden to false for generateUseQueryReturnValueError(), there's obviously plenty of room for outcomes that are "impossible" in "real life".

Quicklime answered 16/2, 2022 at 20:1 Comment(0)
E
3

If you want to mock a method that returns a useQuery, you should do the following: For example we have a hook "useExample" that return an useQuery:

    export const useExample = (props?: ResponseType): UseQueryResult<ResponseModel> =>  
 useQuery<ResponseModel>(
     "get-example",
     () => getExample(),
     {
       ...props,
       select: (data) => data,
     }   );

the mock:

const useExampleMock = (params: UseQueryResult<ResponseModel>) =>
  jest.spyOn(useConfigModule, 'useExample').mockImplementation(() => {
    return params;
  });

using this mock:

export const MOCK_USE_QUERY_RESULT = {
  data: undefined,
  dataUpdatedAt: 0,
  error: null,
  errorUpdatedAt: 0,
  failureCount: 0,
  isError: false,
  isFetched: false,
  isFetchedAfterMount: false,
  isFetching: false,
  isIdle: false,
  isLoading: false,
  isLoadingError: false,
  isPlaceholderData: false,
  isPreviousData: false,
  isRefetchError: false,
  isRefetching: false,
  isStale: false,
  isSuccess: true,
  status: ‘success’,
  refetch: jest.fn(),
  remove: jest.fn(),
};

    useConfigMock({...MOCK_USE_QUERY_RESULT, isError: false, isSuccess: false});
Exhume answered 26/5, 2022 at 21:53 Comment(2)
What is useConfigModule and useConfigMock?Gass
Im guessing useConfigModule is the name of the module that holds useExample (The real code). useConfigMock might be just a typo. It should be useExampleMock.Cesya
A
2

I do not always want to intercept the endpoint, I do agree that in most of the cases it's the best approach. In this example, and others I've had found, the API service + queries/mutations creates a separation of concerns between the payload serialization/aggregation and the actual network request, that in my use cases I do want to unit test and identify. Means, I want to assert that the UI part is calling the mutation with the proper payload, not the API service part aggregating/modifying that payload for backend needs.

I just followed the recommendation from TkDodo's series, exported all my queries and mutations from a single module, and from there mocking and asserting with Jest is pretty much straightforward

const mockOnCreateRule = jest
  .fn()
  .mockResolvedValue({ data: "mocked response" });

// this mock can be improved depending on the consumer component
jest.mock("features/rules/hooks/rule-versions.hooks", () => ({
  useCreateRuleVersion: () => ({
    mutateAsync: mockOnCreateRule,
  }),
}));

and for asserting the mutation payload just something like

await waitFor(() =>
      expect(mockOnCreateRule).toHaveBeenCalledWith(
        expect.objectContaining({
          parameters: expect.arrayContaining([
            { key_name: "thickness", value: "100" },
          ]),
        }),
        // Add anything for the mutation options, or provide them if necessary
        expect.anything()
      )
    );
Abdication answered 2/8, 2022 at 10:14 Comment(0)
L
1

So as you have seen from the other answers, mocking useQuery is pretty straightforward, and I will not repeat their answers.

However, if you have tried it with useMutation, you would immediately realize useMutation isn't called at all if mocked by jest in @apollo/client. I spent the entire afternoon on it until I realized it's not designed that way.

Quoting the official design under ApolloClient's github https://github.com/TanStack/query/discussions/2505:

the docs don't suggest to mock the actual mutation function, but to rather mock the outgoing network requests. You can do that with something like nock or I would suggest mock-service-worker.

As they mentioned, the best way is to mock the network by mock-service-worker. However, all I need is to tell if the mutation is called at all, and I really don't want to introduce that much of complexity handling the network.

So I used ApolliClient's supplied MockedProvider and created a working example for useMutation.

const MOCK_USER_REGISTER_VARIABLES: UserRegisterVariables = {
  firstName: 'Jane',
  lastName: 'Citizen',
};

function createMockRegisterMutation(callback?: () => void) {
  return {
    request: {
      query: USER_REGISTER,
      variables: MOCK_USER_REGISTER_VARIABLES,
    },
    result: () => {
      callback?.();
      return {
        data: MOCK_RESPONSE_DATA,
      };
    },
  };
}

And used it in a jest test like this

describe('MyScreen', async () => {
  it('should submit mutation when clicked button', async () => {

    let isMutationCalled = false;
    const Contents = () => (
      <MockedProvider
        mocks={[
          createMockRegisterMutation(() => {
            isMutationCalled = true;
          }),
        ]}
      >
        <MyScreen />
      </MockedProvider>
    );
    const screen = render(<Contents />);

    fireEvent.changeText(
      screen.getByTestId('first-name-input'),
      MOCK_USER_REGISTER_VARIABLES.firstName
    );

    fireEvent.changeText(
      screen.getByTestId('last-name-input'),
      MOCK_USER_REGISTER_VARIABLES.lastName
    );

    fireEvent.press(screen.getByTestId('submit-button'));

    await waitFor(() => expect(isMutationCalled).toBe(true));
  });
});

pro: Simpler than mocking the network.

con: The request and response are fixed by the MockProvider and cannot be changed dynamically later on in the same test. You would have to create a second test for a different set of request and response.

pro or con: If the mutation variables submitted are not an exact match, the test will simply fail, because the MockedProvider will only be fired with the exact variables. To me it's a pro, since it saved my effort examining the submission parameters, but in other cases it may be inconvenient.

Lithesome answered 6/3, 2023 at 2:58 Comment(0)
W
0

This is what I came up with using just jest mock documentation:

import * as reactQuery from 'react-query'
...  
reactQuery.useQuery = jest.fn().mockReturnValue({ data: { ..your data.. }, isSuccess: true, ..anything else you need from useQuery.. })
 

And that worked for me. Mocking modules and implementations did not help

Windsail answered 11/1, 2023 at 13:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.